mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-22 12:19:09 +00:00
feat: Allow workflow execution even if it has errors (#9037)
This commit is contained in:
@@ -5,9 +5,7 @@ import { setActivePinia } from 'pinia';
|
||||
import type { IStartRunData, IWorkflowData } from '@/Interface';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { IPinData, IRunData, Workflow } from 'n8n-workflow';
|
||||
|
||||
@@ -70,7 +68,6 @@ vi.mock('@/composables/useWorkflowHelpers', () => ({
|
||||
|
||||
vi.mock('@/composables/useNodeHelpers', () => ({
|
||||
useNodeHelpers: vi.fn().mockReturnValue({
|
||||
refreshNodeIssues: vi.fn(),
|
||||
updateNodesExecutionIssues: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
@@ -94,9 +91,7 @@ describe('useRunWorkflow({ router })', () => {
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
let router: ReturnType<typeof useRouter>;
|
||||
let toast: ReturnType<typeof useToast>;
|
||||
let workflowHelpers: ReturnType<typeof useWorkflowHelpers>;
|
||||
let nodeHelpers: ReturnType<typeof useNodeHelpers>;
|
||||
|
||||
beforeAll(() => {
|
||||
const pinia = createTestingPinia();
|
||||
@@ -108,9 +103,7 @@ describe('useRunWorkflow({ router })', () => {
|
||||
workflowsStore = useWorkflowsStore();
|
||||
|
||||
router = useRouter();
|
||||
toast = useToast();
|
||||
workflowHelpers = useWorkflowHelpers({ router });
|
||||
nodeHelpers = useNodeHelpers();
|
||||
});
|
||||
|
||||
describe('runWorkflowApi()', () => {
|
||||
@@ -170,22 +163,26 @@ describe('useRunWorkflow({ router })', () => {
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle workflow issues correctly', async () => {
|
||||
it('should execute workflow even if it has issues', async () => {
|
||||
const mockExecutionResponse = { executionId: '123' };
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
|
||||
vi.mocked(uiStore).isActionActive.mockReturnValue(false);
|
||||
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({
|
||||
name: 'Test Workflow',
|
||||
} as unknown as Workflow);
|
||||
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
|
||||
vi.mocked(workflowsStore).nodesIssuesExist = true;
|
||||
vi.mocked(nodeHelpers).refreshNodeIssues.mockImplementation(() => {});
|
||||
vi.mocked(workflowHelpers).checkReadyForExecution.mockReturnValue({
|
||||
someNode: { issues: { input: ['issue'] } },
|
||||
});
|
||||
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
|
||||
id: 'workflowId',
|
||||
nodes: [],
|
||||
} as unknown as IWorkflowData);
|
||||
vi.mocked(workflowsStore).getWorkflowRunData = {
|
||||
NodeName: [],
|
||||
};
|
||||
|
||||
const result = await runWorkflow({});
|
||||
expect(result).toBeUndefined();
|
||||
expect(toast.showMessage).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockExecutionResponse);
|
||||
});
|
||||
|
||||
it('should execute workflow successfully', async () => {
|
||||
@@ -198,7 +195,6 @@ describe('useRunWorkflow({ router })', () => {
|
||||
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({
|
||||
name: 'Test Workflow',
|
||||
} as Workflow);
|
||||
vi.mocked(nodeHelpers).refreshNodeIssues.mockImplementation(() => {});
|
||||
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
|
||||
id: 'workflowId',
|
||||
nodes: [],
|
||||
|
||||
@@ -11,17 +11,11 @@ import type {
|
||||
IRunExecutionData,
|
||||
ITaskData,
|
||||
IPinData,
|
||||
IWorkflowBase,
|
||||
Workflow,
|
||||
StartNodeData,
|
||||
IRun,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
NodeHelpers,
|
||||
NodeConnectionType,
|
||||
TelemetryHelpers,
|
||||
FORM_TRIGGER_PATH_IDENTIFIER,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionType, FORM_TRIGGER_PATH_IDENTIFIER } from 'n8n-workflow';
|
||||
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
@@ -42,14 +36,12 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import type { useRouter } from 'vue-router';
|
||||
import { isEmpty } from '@/utils/typesUtils';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
export function useRunWorkflow(options: { router: ReturnType<typeof useRouter> }) {
|
||||
export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof useRouter> }) {
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const workflowHelpers = useWorkflowHelpers({ router: options.router });
|
||||
const workflowHelpers = useWorkflowHelpers({ router: useRunWorkflowOpts.router });
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
const toast = useToast();
|
||||
const { titleSet } = useTitleChange();
|
||||
|
||||
@@ -106,79 +98,6 @@ export function useRunWorkflow(options: { router: ReturnType<typeof useRouter> }
|
||||
toast.clearAllStickyNotifications();
|
||||
|
||||
try {
|
||||
// Check first if the workflow has any issues before execute it
|
||||
nodeHelpers.refreshNodeIssues();
|
||||
const issuesExist = workflowsStore.nodesIssuesExist;
|
||||
if (issuesExist) {
|
||||
// If issues exist get all of the issues of all nodes
|
||||
const workflowIssues = workflowHelpers.checkReadyForExecution(
|
||||
workflow,
|
||||
options.destinationNode,
|
||||
);
|
||||
if (workflowIssues !== null) {
|
||||
const errorMessages = [];
|
||||
let nodeIssues: string[];
|
||||
const trackNodeIssues: Array<{
|
||||
node_type: string;
|
||||
error: string;
|
||||
}> = [];
|
||||
const trackErrorNodeTypes: string[] = [];
|
||||
for (const nodeName of Object.keys(workflowIssues)) {
|
||||
nodeIssues = NodeHelpers.nodeIssuesToString(workflowIssues[nodeName]);
|
||||
let issueNodeType = 'UNKNOWN';
|
||||
const issueNode = workflowsStore.getNodeByName(nodeName);
|
||||
|
||||
if (issueNode) {
|
||||
issueNodeType = issueNode.type;
|
||||
}
|
||||
|
||||
trackErrorNodeTypes.push(issueNodeType);
|
||||
const trackNodeIssue = {
|
||||
node_type: issueNodeType,
|
||||
error: '',
|
||||
caused_by_credential: !!workflowIssues[nodeName].credentials,
|
||||
};
|
||||
|
||||
for (const nodeIssue of nodeIssues) {
|
||||
errorMessages.push(
|
||||
`<a data-action='openNodeDetail' data-action-parameter-node='${nodeName}'>${nodeName}</a>: ${nodeIssue}`,
|
||||
);
|
||||
trackNodeIssue.error = trackNodeIssue.error.concat(', ', nodeIssue);
|
||||
}
|
||||
trackNodeIssues.push(trackNodeIssue);
|
||||
}
|
||||
|
||||
toast.showMessage({
|
||||
title: i18n.baseText('workflowRun.showMessage.title'),
|
||||
message: errorMessages.join('<br />'),
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
});
|
||||
titleSet(workflow.name as string, 'ERROR');
|
||||
void useExternalHooks().run('workflowRun.runError', {
|
||||
errorMessages,
|
||||
nodeName: options.destinationNode,
|
||||
});
|
||||
|
||||
await workflowHelpers.getWorkflowDataToSave().then((workflowData) => {
|
||||
telemetry.track('Workflow execution preflight failed', {
|
||||
workflow_id: workflow.id,
|
||||
workflow_name: workflow.name,
|
||||
execution_type: options.destinationNode || options.triggerNode ? 'node' : 'workflow',
|
||||
node_graph_string: JSON.stringify(
|
||||
TelemetryHelpers.generateNodesGraph(
|
||||
workflowData as IWorkflowBase,
|
||||
workflowHelpers.getNodeTypes(),
|
||||
).nodeGraph,
|
||||
),
|
||||
error_node_types: JSON.stringify(trackErrorNodeTypes),
|
||||
errors: JSON.stringify(trackNodeIssues),
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the direct parents of the node
|
||||
let directParentNodes: string[] = [];
|
||||
if (options.destinationNode !== undefined) {
|
||||
@@ -319,7 +238,7 @@ export function useRunWorkflow(options: { router: ReturnType<typeof useRouter> }
|
||||
executedNode,
|
||||
data: {
|
||||
resultData: {
|
||||
runData: newRunData || {},
|
||||
runData: newRunData ?? {},
|
||||
pinData: workflowData.pinData,
|
||||
workflowData,
|
||||
},
|
||||
@@ -372,7 +291,9 @@ export function useRunWorkflow(options: { router: ReturnType<typeof useRouter> }
|
||||
node.parameters.resume === 'form' &&
|
||||
runWorkflowApiResponse.executionId
|
||||
) {
|
||||
const workflowTriggerNodes = workflow.getTriggerNodes().map((node) => node.name);
|
||||
const workflowTriggerNodes = workflow
|
||||
.getTriggerNodes()
|
||||
.map((triggerNode) => triggerNode.name);
|
||||
|
||||
const showForm =
|
||||
options.destinationNode === node.name ||
|
||||
@@ -383,7 +304,7 @@ export function useRunWorkflow(options: { router: ReturnType<typeof useRouter> }
|
||||
|
||||
if (!showForm) continue;
|
||||
|
||||
const { webhookSuffix } = (node.parameters.options || {}) as IDataObject;
|
||||
const { webhookSuffix } = (node.parameters.options ?? {}) as IDataObject;
|
||||
const suffix = webhookSuffix ? `/${webhookSuffix}` : '';
|
||||
testUrl = `${rootStore.getFormWaitingUrl}/${runWorkflowApiResponse.executionId}${suffix}`;
|
||||
}
|
||||
|
||||
@@ -515,7 +515,7 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
||||
return count;
|
||||
}
|
||||
|
||||
// Checks if everything in the workflow is complete and ready to be executed
|
||||
/** Checks if everything in the workflow is complete and ready to be executed */
|
||||
function checkReadyForExecution(workflow: Workflow, lastNodeName?: string) {
|
||||
let node: INode;
|
||||
let nodeType: INodeType | undefined;
|
||||
|
||||
Reference in New Issue
Block a user