Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 72e8175d9a Field creation fails with unhandled duplicate key error due to stale cache validation
https://sonarly.com/issue/8579?type=bug

Creating a field via POST /metadata fails when a field with the same name already exists on the same object, because the name-uniqueness validation checks a stale in-memory cache rather than the database, and the resulting DB constraint violation is not handled as a user-friendly error.

Fix: The fix adds handling for `WorkspaceMigrationRunnerException` in `fieldMetadataGraphqlApiExceptionHandler`. Previously, when the DB unique constraint `IDX_FIELD_METADATA_NAME_OBJECT_METADATA_ID_WORKSPACE_ID_UNIQUE` was violated (due to the stale-cache TOCTOU race condition), the resulting `WorkspaceMigrationRunnerException` fell through the handler unhandled and propagated to Sentry as an opaque error.

The fix intercepts that specific exception — where `code === EXECUTION_FAILED` and `errors.metadata.message` contains the unique constraint name — and converts it to a user-friendly `ConflictError("A field with this name already exists on this object")`.

This is the minimal, targeted fix: it handles the DB constraint as the last line of defense, which is exactly the role the constraint was added to serve, without touching the cache layer or the migration runner itself.

```typescript file=packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util.ts lines=17-40
import {
  WorkspaceMigrationRunnerException,
  WorkspaceMigrationRunnerExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';

// ...

  if (
    error instanceof WorkspaceMigrationRunnerException &&
    error.code === WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED &&
    error.errors?.metadata?.message?.includes(
      'IDX_FIELD_METADATA_NAME_OBJECT_METADATA_ID_WORKSPACE_ID_UNIQUE',
    )
  ) {
    throw new ConflictError(
      new Error('A field with this name already exists on this object'),
    );
  }
```
2026-03-04 04:00:05 +00:00
Sonarly Claude Code 0c39034fa7 Nested DragDropContexts cause invariant crash during drag operations
https://sonarly.com/issue/8281?type=bug

The `@hello-pangea/dnd` library throws "Invariant failed" because multiple `DragDropContext` components are nested inside each other, which the library does not support. The outer `PageDragDropProvider` wraps the entire page layout including inner record table/board drag contexts.
2026-03-04 03:57:49 +00:00
Sonarly Claude Code b3b0c27f66 ViewField delete fails: runner loads stale cache after DB cascade deletes viewFields
https://sonarly.com/issue/3344?type=bug

When deactivating a field, the migration runner independently loads flat entity maps from cache to resolve viewField universal identifiers, but a concurrent operation (or the same migration's earlier view delete with DB CASCADE) can remove the viewField from the underlying data between the builder and runner cache reads, causing a lookup failure.

Fix: ## Root Cause

The orchestrator was ordering view-related delete actions with parent `view.delete` **before** child `viewField.delete` (and other child entity deletes). Because `ViewFieldEntity.view` has `onDelete: 'CASCADE'`, when the runner executes `view.delete` it issues a hard `DELETE` against the `view` table; the database CASCADE automatically hard-deletes all child `viewField` rows. However, the runner's optimistic cache update only removes the view from `flatViewMaps` — it does **not** cascade-remove the viewFields from `flatViewFieldMaps`. This leaves a stale/inconsistent optimistic cache.

Then when the runner processes the subsequent `viewField.delete` action, `transpileUniversalDeleteActionToFlatDeleteAction` tries to look up the viewField by its universal identifier in the (now stale) `allFlatEntityMaps`. Since the DB already deleted it via CASCADE, any concurrent operation or cache invalidation that triggers a fresh load will produce a snapshot missing the viewField, causing `findFlatEntityByUniversalIdentifierOrThrow` to throw — wrapped as `WorkspaceMigrationRunnerException` with message `"Migration action 'delete' for 'viewField' failed"`.

## Fix

Reorder the view-related actions in `workspace-migration-build-orchestrator.service.ts` so that all child-entity deletes (`viewFilter`, `viewFilterGroup`, `viewGroup`, `viewFieldGroup`, `viewField`) are executed **before** `view.delete`. This ensures:

1. Child entities are explicitly deleted and removed from the optimistic cache first.
2. When `view.delete` runs and the DB CASCADE fires, there are no remaining child rows in either the DB or the optimistic cache — both are already consistent.
3. The subsequent (now empty) child-delete lists are no-ops, maintaining correctness.

```typescript file=packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts lines=747-768
// Views
// Child entities must be deleted before parent views to prevent
// DB CASCADE from removing them before the runner's optimistic
// cache processes those delete actions (causing lookup failures).
...aggregatedOrchestratorActionsReport.viewFilter.delete,
...aggregatedOrchestratorActionsReport.viewFilterGroup.delete,
...aggregatedOrchestratorActionsReport.viewGroup.delete,
...aggregatedOrchestratorActionsReport.viewFieldGroup.delete,
...aggregatedOrchestratorActionsReport.viewField.delete,
...aggregatedOrchestratorActionsReport.view.delete,
...aggregatedOrchestratorActionsReport.view.create,
...aggregatedOrchestratorActionsReport.view.update,
...aggregatedOrchestratorActionsReport.viewFieldGroup.create,
...aggregatedOrchestratorActionsReport.viewFieldGroup.update,
...aggregatedOrchestratorActionsReport.viewField.create,
...aggregatedOrchestratorActionsReport.viewField.update,
...aggregatedOrchestratorActionsReport.viewFilterGroup.create,
...aggregatedOrchestratorActionsReport.viewFilterGroup.update,
...aggregatedOrchestratorActionsReport.viewFilter.create,
...aggregatedOrchestratorActionsReport.viewFilter.update,
...aggregatedOrchestratorActionsReport.viewGroup.create,
...aggregatedOrchestratorActionsReport.viewGroup.update,
```

Note: This fix eliminates the intra-migration ordering problem. The separate TOCTOU race between concurrent migrations (independent builder and runner cache reads) remains a narrower architectural gap, but the deterministic failure path described in the Sentry error is resolved by this ordering fix.
2026-03-04 03:52:18 +00:00
@@ -14,6 +14,10 @@ import {
import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { workspaceMigrationBuilderExceptionFormatter } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter';
import {
WorkspaceMigrationRunnerException,
WorkspaceMigrationRunnerExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
export const fieldMetadataGraphqlApiExceptionHandler = (
error: Error,
@@ -23,6 +27,18 @@ export const fieldMetadataGraphqlApiExceptionHandler = (
workspaceMigrationBuilderExceptionFormatter(error, i18n);
}
if (
error instanceof WorkspaceMigrationRunnerException &&
error.code === WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED &&
error.errors?.metadata?.message?.includes(
'IDX_FIELD_METADATA_NAME_OBJECT_METADATA_ID_WORKSPACE_ID_UNIQUE',
)
) {
throw new ConflictError(
new Error('A field with this name already exists on this object'),
);
}
if (error instanceof InvalidMetadataException) {
throw new UserInputError(error);
}