Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 809c0540f6 Unbounded staled workflow runs query exceeds 200-record update limit
https://sonarly.com/issue/3797?type=bug

The `handleStaledRunsForWorkspace` method fetches all staled workflow runs without any limit, then passes all their IDs to a single `update()` call which has a hard ORM limit of 200 records, causing the job to fail for workspaces with >200 staled runs.

Fix: The fix adds `take: QUERY_MAX_RECORDS` to the unbounded `find()` call in `handleStaledRunsForWorkspace`, capping the number of staled workflow runs fetched (and subsequently updated) to 200 — the same hard limit enforced by `WorkspaceUpdateQueryBuilder.execute()`.

```typescript file=packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/workspace-services/workflow-handle-staled-runs.workspace-service.ts lines=35-38
const staledWorkflowRuns = await workflowRunRepository.find({
  where: getStaledRunsFindOptions(),
  take: QUERY_MAX_RECORDS,
});
```

`QUERY_MAX_RECORDS` (= 200) is imported from `twenty-shared/constants` — the same constant the ORM guard uses — so the two limits stay in sync automatically if the constant ever changes.

Any staled runs beyond the first 200 will simply be picked up on the next hourly cron cycle. This is safe because the cron job already uses `repository.exists()` to guard whether work is needed; workspaces with >200 staled runs will continue to be scheduled each hour until all runs are cleared, making progress of exactly 200 records per cycle.
2026-03-04 17:58:20 +00:00
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
@@ -1,5 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
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';
import {
@@ -32,6 +34,7 @@ export class WorkflowHandleStaledRunsWorkspaceService {
const staledWorkflowRuns = await workflowRunRepository.find({
where: getStaledRunsFindOptions(),
take: QUERY_MAX_RECORDS,
});
if (staledWorkflowRuns.length <= 0) {