Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code ede012f9ba Staled workflow runs handler lacks batching, fails when >200 runs accumulate
https://sonarly.com/issue/3797?type=bug

The `WorkflowHandleStaledRunsWorkspaceService.handleStaledRunsForWorkspace()` fetches all staled workflow runs without a limit and passes all IDs to a single `update()` call, which hits the ORM's hard 200-record update limit, preventing recovery of stuck workflow runs.

Fix: Replaced the unbounded `find()` + single `update()` pattern with a batched do-while loop that processes staled workflow runs in chunks of `QUERY_MAX_RECORDS` (200).

**What changed:**
- Added `import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'` (same import used by sibling `workflow-run-enqueue.workspace-service.ts`)
- Changed the `find()` call to include `select: { id: true }` and `take: QUERY_MAX_RECORDS` to limit each batch to 200 records
- Wrapped the find+update in a `do-while` loop that continues fetching and updating batches until fewer than `QUERY_MAX_RECORDS` are returned (meaning all staled runs have been processed)
- Added a guard so `recomputeWorkflowRunNotStartedCount` is only called if at least one record was updated

**Why this works:**
Each batch updates at most 200 records, staying within the `WorkspaceUpdateQueryBuilder`'s `QUERY_MAX_RECORDS` limit. After each batch is updated (status changed from `ENQUEUED` → `NOT_STARTED`), those records no longer match `getStaledRunsFindOptions()`, so the next `find()` naturally returns the next set of staled runs. This is the same pattern used by `workflow-run-enqueue.workspace-service.ts` (lines 83-98) which already works correctly in production.
2026-03-07 06:22:09 +00:00
Sonarly Claude Code 347a084f2c Orphaned view sorts crash People page after field deactivation
https://sonarly.com/issue/3162?type=bug

When a field is deactivated/deleted, the server-side cleanup omits view sorts referencing that field, leaving orphaned view sorts in the database. The frontend's `EditableSortChip` throws an unhandled exception when trying to render these orphaned sorts, crashing the entire page.

Fix: ## Changes

### Server-side: Add view sort cleanup to field deactivation side effects

**`handle-field-metadata-deactivation-side-effects.util.ts`** — Added `flatViewSortMaps` to the function's input args (via `Pick<AllFlatEntityMaps, ...>`), added `FlatViewSort` import, added `flatViewSortsToDelete` to the return type and implementation. Uses `fromFlatFieldMetadata.viewSortIds` (which was already populated but never consumed) to find and return the view sorts to delete.

**`handle-flat-field-metadata-update-side-effect.util.ts`** — Added `flatViewSortMaps` to args, `flatViewSortsToDelete: []` to empty side effects constant, and propagation of `flatViewSortsToDelete` from the deactivation handler into the result.

**`from-update-field-input-to-flat-field-metadata.util.ts`** — Added `flatViewSortMaps` to the args type, destructured it, passed it through to `handleFlatFieldMetadataUpdateSideEffect`, and accumulated `flatViewSortsToDelete` in the reduce result.

**`field-metadata.service.ts`** — Added `flatViewSortMaps` to the cache fetch keys and destructuring, passed it to `fromUpdateFieldInputToFlatFieldMetadata`, and added a `viewSort` entry in `allFlatEntityOperationByMetadataName` with `flatEntityToDelete: flatViewSortsToDelete`.

### Frontend: Defense-in-depth filter for orphaned sorts

**`ViewBarRecordSortEffect.tsx`** — Before setting `currentRecordSorts`, filters `currentView.viewSorts` to only include sorts whose `fieldMetadataId` exists in `objectMetadataItem.fields`. This prevents the crash for any orphaned sorts that already exist in the database (before the server-side fix takes effect for new deactivations).
2026-03-07 06:03:51 +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 {
@@ -30,25 +32,36 @@ export class WorkflowHandleStaledRunsWorkspaceService {
{ shouldBypassPermissionChecks: true },
);
const staledWorkflowRuns = await workflowRunRepository.find({
where: getStaledRunsFindOptions(),
});
let updatedCount = 0;
let batchRuns: WorkflowRunWorkspaceEntity[];
if (staledWorkflowRuns.length <= 0) {
return;
do {
batchRuns = await workflowRunRepository.find({
where: getStaledRunsFindOptions(),
select: { id: true },
take: QUERY_MAX_RECORDS,
});
if (batchRuns.length === 0) {
break;
}
await workflowRunRepository.update(
batchRuns.map((workflowRun) => workflowRun.id),
{
enqueuedAt: null,
status: WorkflowRunStatus.NOT_STARTED,
},
);
updatedCount += batchRuns.length;
} while (batchRuns.length === QUERY_MAX_RECORDS);
if (updatedCount > 0) {
await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount(
workspaceId,
);
}
await workflowRunRepository.update(
staledWorkflowRuns.map((workflowRun) => workflowRun.id),
{
enqueuedAt: null,
status: WorkflowRunStatus.NOT_STARTED,
},
);
await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount(
workspaceId,
);
}, authContext);
}
}