Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code bcf89d737f chore: additional changes for Workflow destroy fails when logic function already 2026-03-06 20:51:06 +00:00
Sonarly Claude Code 85bc9bfad7 chore: improve monitoring for Workflow destroy fails when logic function already
Applied the same idempotent error handling pattern to `runWorkflowVersionStepDeletionSideEffects` in `workflow-version-step-operations.workspace-service.ts`. This is the other code path that calls `deleteOneWithSource` during step cleanup.

Without this fix, deleting a CODE step whose logic function was already removed (e.g., due to a prior workflow version operation or a race condition) would throw an unhandled `LogicFunctionException` that surfaces to Sentry as a noisy error — even though the desired end state (logic function gone) has already been achieved.

Changes:
- Added `Logger` instance and `LogicFunctionException`/`LogicFunctionExceptionCode` imports
- Wrapped the `deleteOneWithSource` call in a try/catch that only catches `LOGIC_FUNCTION_NOT_FOUND`
- Added `logger.warn` with the specific `logicFunctionId` to provide structured observability instead of noisy Sentry error captures
- All other exceptions continue to propagate normally
2026-03-06 20:51:04 +00:00
Sonarly Claude Code 51d888387d Workflow destroy fails when logic function already deleted across versions
https://sonarly.com/issue/5446?type=bug

The `DestroyOneWorkflow` mutation fails because `handleLogicFunctionSubEntities` iterates all workflow versions' CODE steps and calls `destroyOne` for each logic function ID without deduplication or error handling, causing a fatal exception when a referenced logic function no longer exists.

Fix: Added idempotent error handling to `handleLogicFunctionSubEntities` in `workflow-common.workspace-service.ts` — the exact code path shown in the Sentry stack trace.

The method iterates all workflow versions (including soft-deleted) and destroys each CODE step's logic function. When a `logicFunctionId` is already deleted (either by a prior iteration destroying the same shared ID, or by an independent deletion), `deleteOneWithSource` throws `LogicFunctionException` with code `LOGIC_FUNCTION_NOT_FOUND`. The fix wraps the call in a try/catch that:
- Catches only `LogicFunctionException` with `LOGIC_FUNCTION_NOT_FOUND` — the "already gone" case is a valid idempotent outcome for cleanup
- Re-throws all other exceptions — genuine errors (DB failures, permission issues, etc.) still propagate
- Logs a warning with the specific `logicFunctionId` and `workflowId` for observability

This matches the identical pattern already applied (but not yet merged to this branch) in commit `6743ee194f` for the step deletion side effects path.
2026-03-06 20:51:04 +00:00
Sonarly Claude Code 1181c35105 chore: improve monitoring for Non-idempotent ActivateWorkspace mutation fails on
Added `!isSubmitting` guard to the `handleKeyDown` Enter key handler in `CreateWorkspace.tsx`. The button already had `disabled={!isValid || isSubmitting}`, but the Enter key handler bypassed this check, allowing duplicate mutation submissions when pressing Enter rapidly. This reduces noise from duplicate submissions reaching the server.
2026-03-06 20:46:58 +00:00
Sonarly Claude Code 56c5a69694 Non-idempotent ActivateWorkspace mutation fails on retry after successful activation
https://sonarly.com/issue/4069?type=bug

The `activateWorkspace` mutation throws "Workspace is not pending creation" when called a second time because the workspace was already successfully activated by a prior request. The operation is not idempotent, and multiple retry paths exist in the frontend.

Fix: Made `activateWorkspace` idempotent for the `ACTIVE` state. When the workspace is already active (from a prior successful activation), the method now returns the existing workspace entity instead of throwing an error. This handles all retry scenarios: Apollo RetryLink auto-retries, user retries after post-activation frontend failures, and double-submissions.

The check is placed **before** the existing `ONGOING_CREATION` and `PENDING_CREATION` checks, so:
- `ACTIVE` → returns workspace (new idempotent behavior)
- `ONGOING_CREATION` → still throws "Workspace is already being created"
- `INACTIVE` / `SUSPENDED` → still throws "Workspace is not pending creation"

A `logger.log` call is added to track when the idempotent path is taken, providing observability without generating Sentry errors.
2026-03-06 20:46:58 +00:00
2 changed files with 52 additions and 10 deletions
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
@@ -8,6 +8,10 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
@@ -40,6 +44,8 @@ export type ObjectMetadataInfo = {
@Injectable()
export class WorkflowCommonWorkspaceService {
private readonly logger = new Logger(WorkflowCommonWorkspaceService.name);
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
@@ -322,10 +328,24 @@ export class WorkflowCommonWorkspaceService {
for (const workflowVersion of workflowVersions) {
for (const step of workflowVersion.steps ?? []) {
if (step.type === WorkflowActionType.CODE) {
await this.logicFunctionFromSourceService.deleteOneWithSource({
id: step.settings.input.logicFunctionId,
workspaceId,
});
try {
await this.logicFunctionFromSourceService.deleteOneWithSource({
id: step.settings.input.logicFunctionId,
workspaceId,
});
} catch (error) {
if (
error instanceof LogicFunctionException &&
error.code ===
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND
) {
this.logger.warn(
`Logic function ${step.settings.input.logicFunctionId} already deleted, skipping cleanup for workflow ${workflowId}`,
);
} else {
throw error;
}
}
}
}
}
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
@@ -21,6 +21,10 @@ import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -63,6 +67,10 @@ const ITERATOR_EMPTY_STEP_POSITION_OFFSET = {
@Injectable()
export class WorkflowVersionStepOperationsWorkspaceService {
private readonly logger = new Logger(
WorkflowVersionStepOperationsWorkspaceService.name,
);
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
@@ -87,10 +95,24 @@ export class WorkflowVersionStepOperationsWorkspaceService {
}) {
switch (step.type) {
case WorkflowActionType.CODE: {
await this.logicFunctionFromSourceService.deleteOneWithSource({
id: step.settings.input.logicFunctionId,
workspaceId,
});
try {
await this.logicFunctionFromSourceService.deleteOneWithSource({
id: step.settings.input.logicFunctionId,
workspaceId,
});
} catch (error) {
if (
error instanceof LogicFunctionException &&
error.code ===
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND
) {
this.logger.warn(
`Logic function ${step.settings.input.logicFunctionId} already deleted, skipping cleanup`,
);
} else {
throw error;
}
}
break;
}
case WorkflowActionType.AI_AGENT: {