fix: return 404 for deleted workspace webhook race (#19439)
Handle late TwentyORM workspace-not-found exceptions in the workflow webhook REST exception filter so deleted workspaces return a 404 instead of surfacing as internal errors. Also add a focused regression spec covering the deleted-workspace ORM codes and the existing workflow-trigger status mappings. Closes #15544 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+103
-75
@@ -20,6 +20,10 @@ import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import {
|
||||
TwentyORMException,
|
||||
TwentyORMExceptionCode,
|
||||
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import {
|
||||
WorkflowVersionStatus,
|
||||
@@ -91,90 +95,114 @@ export class WorkflowTriggerController {
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
const { workflow } =
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const workflowRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflow',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
try {
|
||||
const { workflow } =
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const workflowRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflow',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflow = await workflowRepository.findOne({
|
||||
where: { id: workflowId },
|
||||
});
|
||||
const workflow = await workflowRepository.findOne({
|
||||
where: { id: workflowId },
|
||||
});
|
||||
|
||||
if (!isDefined(workflow)) {
|
||||
throw new WorkflowTriggerException(
|
||||
`[Webhook trigger] Workflow ${workflowId} not found in workspace ${workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
if (!isDefined(workflow)) {
|
||||
throw new WorkflowTriggerException(
|
||||
`[Webhook trigger] Workflow ${workflowId} not found in workspace ${workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!isDefined(workflow.lastPublishedVersionId) ||
|
||||
workflow.lastPublishedVersionId === ''
|
||||
) {
|
||||
throw new WorkflowTriggerException(
|
||||
`[Webhook trigger] Workflow ${workflowId} has not been activated in workspace ${workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS,
|
||||
);
|
||||
}
|
||||
if (
|
||||
!isDefined(workflow.lastPublishedVersionId) ||
|
||||
workflow.lastPublishedVersionId === ''
|
||||
) {
|
||||
throw new WorkflowTriggerException(
|
||||
`[Webhook trigger] Workflow ${workflowId} has not been activated in workspace ${workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS,
|
||||
);
|
||||
}
|
||||
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
const workflowVersion = await workflowVersionRepository.findOne({
|
||||
where: { id: workflow.lastPublishedVersionId },
|
||||
});
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
const workflowVersion = await workflowVersionRepository.findOne({
|
||||
where: { id: workflow.lastPublishedVersionId },
|
||||
});
|
||||
|
||||
if (!isDefined(workflowVersion)) {
|
||||
throw new WorkflowTriggerException(
|
||||
`[Webhook trigger] No workflow version activated for workflow ${workflowId} in workspace ${workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
|
||||
);
|
||||
}
|
||||
if (!isDefined(workflowVersion)) {
|
||||
throw new WorkflowTriggerException(
|
||||
`[Webhook trigger] No workflow version activated for workflow ${workflowId} in workspace ${workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
|
||||
);
|
||||
}
|
||||
|
||||
if (workflowVersion.trigger?.type !== WorkflowTriggerType.WEBHOOK) {
|
||||
throw new WorkflowTriggerException(
|
||||
`[Webhook trigger] Workflow ${workflowId} does not have a Webhook trigger in workspace ${workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
);
|
||||
}
|
||||
if (workflowVersion.trigger?.type !== WorkflowTriggerType.WEBHOOK) {
|
||||
throw new WorkflowTriggerException(
|
||||
`[Webhook trigger] Workflow ${workflowId} does not have a Webhook trigger in workspace ${workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
);
|
||||
}
|
||||
|
||||
if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) {
|
||||
throw new WorkflowTriggerException(
|
||||
`[Webhook trigger] Workflow version ${workflowVersion.id} is not active in workspace ${workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS,
|
||||
);
|
||||
}
|
||||
if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) {
|
||||
throw new WorkflowTriggerException(
|
||||
`[Webhook trigger] Workflow version ${workflowVersion.id} is not active in workspace ${workspaceId}`,
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS,
|
||||
);
|
||||
}
|
||||
|
||||
return { workflow, workflowVersion };
|
||||
},
|
||||
authContext,
|
||||
return { workflow, workflowVersion };
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
|
||||
const { workflowRunId } =
|
||||
await this.workflowTriggerWorkspaceService.runWorkflowVersion({
|
||||
workflowVersionId: workflow.lastPublishedVersionId!,
|
||||
payload: payload || {},
|
||||
createdBy: {
|
||||
source: FieldActorSource.WEBHOOK,
|
||||
workspaceMemberId: null,
|
||||
name: 'Webhook',
|
||||
context: {},
|
||||
},
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
workflowName: workflow.name,
|
||||
success: true,
|
||||
workflowRunId,
|
||||
};
|
||||
} catch (error) {
|
||||
this.rethrowWorkspaceNotFoundAsTriggerException(error, workspaceId);
|
||||
}
|
||||
}
|
||||
|
||||
private rethrowWorkspaceNotFoundAsTriggerException(
|
||||
error: unknown,
|
||||
workspaceId: string,
|
||||
): never {
|
||||
if (
|
||||
error instanceof TwentyORMException &&
|
||||
[
|
||||
TwentyORMExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
TwentyORMExceptionCode.WORKSPACE_SCHEMA_NOT_FOUND,
|
||||
].includes(error.code)
|
||||
) {
|
||||
throw new WorkflowTriggerException(
|
||||
`[Webhook trigger] Workspace ${workspaceId} not found`,
|
||||
WorkflowTriggerExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const { workflowRunId } =
|
||||
await this.workflowTriggerWorkspaceService.runWorkflowVersion({
|
||||
workflowVersionId: workflow.lastPublishedVersionId!,
|
||||
payload: payload || {},
|
||||
createdBy: {
|
||||
source: FieldActorSource.WEBHOOK,
|
||||
workspaceMemberId: null,
|
||||
name: 'Webhook',
|
||||
context: {},
|
||||
},
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
workflowName: workflow.name,
|
||||
success: true,
|
||||
workflowRunId,
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -9,11 +9,11 @@ import { type Response } from 'express';
|
||||
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
|
||||
import { type CustomException } from 'src/utils/custom-exception';
|
||||
import {
|
||||
type WorkflowTriggerException,
|
||||
WorkflowTriggerException,
|
||||
WorkflowTriggerExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
|
||||
|
||||
@Catch()
|
||||
@Catch(WorkflowTriggerException)
|
||||
export class WorkflowTriggerRestApiExceptionFilter implements ExceptionFilter {
|
||||
constructor(
|
||||
private readonly httpExceptionHandlerService: HttpExceptionHandlerService,
|
||||
|
||||
Reference in New Issue
Block a user