Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 2d209f1836 UpdateWorkflowVersionStep returns client input step without position validation
https://sonarly.com/issue/4115?type=bug

The UpdateWorkflowVersionStep mutation returns the raw client input step as the GraphQL response without merging position data from the existing step or validating the position object, causing a non-nullable field violation when position is present but incomplete.

Fix: ## Fix: Normalize `position` before returning and persisting the updated step

The root cause is that `updateWorkflowVersionStep` returns the raw client input step (via `enrichOutputSchema`, which is a no-op for non-ITERATOR types) directly as the GraphQL mutation response. When the client sends a `position` object that is present but incomplete (e.g. `{}` or `{x: null}`), the GraphQL executor throws `Cannot return null for non-nullable field WorkflowStepPosition.x` because the schema defines `x: Float!` as non-nullable.

The fix adds a `resolvedPosition` check after `updatedStep` is obtained: if `updatedStep.position` is defined but `x` or `y` is not, fall back to `existingStep.position` (the step stored in the DB). This uses the already-imported `isDefined` utility. The `normalizedUpdatedStep` is then used both for persisting to the DB (`updatedSteps`) and as the return value, also fixing the secondary issue of position data loss during persistence.

```typescript file=packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-update.workspace-service.ts lines=76-105
    const resolvedPosition =
      isDefined(updatedStep.position) &&
      isDefined(updatedStep.position.x) &&
      isDefined(updatedStep.position.y)
        ? updatedStep.position
        : existingStep.position;

    const normalizedUpdatedStep = { ...updatedStep, position: resolvedPosition };

    const updatedSteps = workflowVersion.steps.map((existingStep) => {
      if (existingStep.id === step.id) {
        return normalizedUpdatedStep;
      } else {
        return existingStep;
      }
    });
    ...
    return normalizedUpdatedStep;
```

This mirrors the pattern already used in `updateWorkflowVersionStepType`, which explicitly sets `position: existingStep.position` when building the updated step.
2026-03-04 10:51:46 +00:00
@@ -73,9 +73,18 @@ export class WorkflowVersionStepUpdateWorkspaceService {
additionalCreatedSteps: undefined,
};
const resolvedPosition =
isDefined(updatedStep.position) &&
isDefined(updatedStep.position.x) &&
isDefined(updatedStep.position.y)
? updatedStep.position
: existingStep.position;
const normalizedUpdatedStep = { ...updatedStep, position: resolvedPosition };
const updatedSteps = workflowVersion.steps.map((existingStep) => {
if (existingStep.id === step.id) {
return updatedStep;
return normalizedUpdatedStep;
} else {
return existingStep;
}
@@ -93,7 +102,7 @@ export class WorkflowVersionStepUpdateWorkspaceService {
},
);
return updatedStep;
return normalizedUpdatedStep;
}
private async updateWorkflowVersionStepType({