mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-31 08:46:58 +02:00
chore(core): Establish execution context for workflows (#21729)
This commit is contained in:
parent
b3af602ed0
commit
7f976f4399
|
|
@ -0,0 +1,345 @@
|
|||
import { mock } from 'jest-mock-extended';
|
||||
import {
|
||||
UnexpectedError,
|
||||
type INode,
|
||||
type IRunExecutionData,
|
||||
type IWorkflowExecuteAdditionalData,
|
||||
type Workflow,
|
||||
type WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { establishExecutionContext } from '../execution-context';
|
||||
|
||||
describe('establishExecutionContext', () => {
|
||||
const mockWorkflow = mock<Workflow>({ id: 'test-workflow-id' });
|
||||
const mockAdditionalData = mock<IWorkflowExecuteAdditionalData>();
|
||||
const mockMode: WorkflowExecuteMode = 'manual';
|
||||
|
||||
describe('successful context establishment', () => {
|
||||
it('should establish context with version 1 and timestamp', async () => {
|
||||
const startNode = mock<INode>({ name: 'Start', type: 'n8n-nodes-base.start' });
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
startData: {},
|
||||
resultData: {
|
||||
runData: {},
|
||||
},
|
||||
executionData: {
|
||||
contextData: {},
|
||||
nodeExecutionStack: [
|
||||
{
|
||||
node: startNode,
|
||||
data: {
|
||||
main: [[{ json: {} }]],
|
||||
},
|
||||
source: null,
|
||||
},
|
||||
],
|
||||
metadata: {},
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
};
|
||||
|
||||
const beforeTimestamp = Date.now();
|
||||
await establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode);
|
||||
const afterTimestamp = Date.now();
|
||||
|
||||
expect(runExecutionData.executionData!.runtimeData).toBeDefined();
|
||||
expect(runExecutionData.executionData!.runtimeData).toHaveProperty('version', 1);
|
||||
expect(runExecutionData.executionData!.runtimeData).toHaveProperty('establishedAt');
|
||||
expect(runExecutionData.executionData!.runtimeData).toHaveProperty('source', 'manual');
|
||||
|
||||
const establishedAt = runExecutionData.executionData!.runtimeData!.establishedAt;
|
||||
expect(establishedAt).toBeGreaterThanOrEqual(beforeTimestamp);
|
||||
expect(establishedAt).toBeLessThanOrEqual(afterTimestamp);
|
||||
});
|
||||
|
||||
it('should mutate the provided runExecutionData object', async () => {
|
||||
const startNode = mock<INode>({ name: 'Start', type: 'n8n-nodes-base.start' });
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
startData: {},
|
||||
resultData: {
|
||||
runData: {},
|
||||
},
|
||||
executionData: {
|
||||
contextData: {},
|
||||
nodeExecutionStack: [
|
||||
{
|
||||
node: startNode,
|
||||
data: {
|
||||
main: [[{ json: {} }]],
|
||||
},
|
||||
source: null,
|
||||
},
|
||||
],
|
||||
metadata: {},
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
};
|
||||
|
||||
const referenceBefore = runExecutionData.executionData;
|
||||
await establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode);
|
||||
const referenceAfter = runExecutionData.executionData;
|
||||
|
||||
// Verify the same object reference is used (mutation, not replacement)
|
||||
expect(referenceBefore).toBe(referenceAfter);
|
||||
expect(runExecutionData.executionData!.runtimeData).toBeDefined();
|
||||
});
|
||||
|
||||
it('should establish context when execution stack has multiple nodes', async () => {
|
||||
const startNode = mock<INode>({ name: 'Start', type: 'n8n-nodes-base.start' });
|
||||
const secondNode = mock<INode>({ name: 'Second', type: 'n8n-nodes-base.set' });
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
startData: {},
|
||||
resultData: {
|
||||
runData: {},
|
||||
},
|
||||
executionData: {
|
||||
contextData: {},
|
||||
nodeExecutionStack: [
|
||||
{
|
||||
node: startNode,
|
||||
data: {
|
||||
main: [[{ json: {} }]],
|
||||
},
|
||||
source: null,
|
||||
},
|
||||
{
|
||||
node: secondNode,
|
||||
data: {
|
||||
main: [[{ json: {} }]],
|
||||
},
|
||||
source: null,
|
||||
},
|
||||
],
|
||||
metadata: {},
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
};
|
||||
|
||||
await establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode);
|
||||
|
||||
expect(runExecutionData.executionData!.runtimeData).toBeDefined();
|
||||
expect(runExecutionData.executionData!.runtimeData!.version).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Chat Trigger workflow support', () => {
|
||||
it('should establish basic context for empty execution stack (Chat Trigger)', async () => {
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
startData: {},
|
||||
resultData: {
|
||||
runData: {},
|
||||
},
|
||||
executionData: {
|
||||
contextData: {},
|
||||
nodeExecutionStack: [], // Empty stack - Chat Trigger workflow
|
||||
metadata: {},
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
};
|
||||
|
||||
const beforeTimestamp = Date.now();
|
||||
await establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode);
|
||||
const afterTimestamp = Date.now();
|
||||
|
||||
// Should establish basic context even with empty stack
|
||||
expect(runExecutionData.executionData!.runtimeData).toBeDefined();
|
||||
expect(runExecutionData.executionData!.runtimeData!.version).toBe(1);
|
||||
expect(runExecutionData.executionData!.runtimeData!.source).toBe('manual');
|
||||
expect(runExecutionData.executionData!.runtimeData!.establishedAt).toBeGreaterThanOrEqual(
|
||||
beforeTimestamp,
|
||||
);
|
||||
expect(runExecutionData.executionData!.runtimeData!.establishedAt).toBeLessThanOrEqual(
|
||||
afterTimestamp,
|
||||
);
|
||||
});
|
||||
|
||||
it('should establish basic context without start node extraction for Chat Trigger', async () => {
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
startData: {},
|
||||
resultData: {
|
||||
runData: {},
|
||||
},
|
||||
executionData: {
|
||||
contextData: {},
|
||||
nodeExecutionStack: [], // Empty stack
|
||||
metadata: {},
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
};
|
||||
|
||||
await establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode);
|
||||
|
||||
const context = runExecutionData.executionData!.runtimeData;
|
||||
|
||||
// Verify context has only basic properties (no start-node-specific extraction)
|
||||
expect(Object.keys(context!)).toEqual(['version', 'establishedAt', 'source']);
|
||||
expect(typeof context!.version).toBe('number');
|
||||
expect(typeof context!.establishedAt).toBe('number');
|
||||
expect(context!.source).toBe('manual');
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should throw error when executionData is missing', async () => {
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
startData: {},
|
||||
resultData: {
|
||||
runData: {},
|
||||
},
|
||||
// executionData is missing
|
||||
};
|
||||
|
||||
await expect(
|
||||
establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode),
|
||||
).rejects.toThrow(UnexpectedError);
|
||||
});
|
||||
|
||||
it('should throw error when executionData is undefined', async () => {
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
startData: {},
|
||||
resultData: {
|
||||
runData: {},
|
||||
},
|
||||
executionData: undefined,
|
||||
};
|
||||
|
||||
await expect(
|
||||
establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode),
|
||||
).rejects.toThrow(UnexpectedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('context structure validation', () => {
|
||||
it('should create context with correct structure', async () => {
|
||||
const startNode = mock<INode>({ name: 'Webhook', type: 'n8n-nodes-base.webhook' });
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
startData: {},
|
||||
resultData: {
|
||||
runData: {},
|
||||
},
|
||||
executionData: {
|
||||
contextData: {},
|
||||
nodeExecutionStack: [
|
||||
{
|
||||
node: startNode,
|
||||
data: {
|
||||
main: [[{ json: { test: 'data' } }]],
|
||||
},
|
||||
source: null,
|
||||
},
|
||||
],
|
||||
metadata: {},
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
};
|
||||
|
||||
await establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode);
|
||||
|
||||
const context = runExecutionData.executionData!.runtimeData;
|
||||
|
||||
// Verify context has only expected properties
|
||||
expect(Object.keys(context!)).toEqual(['version', 'establishedAt', 'source']);
|
||||
expect(typeof context!.version).toBe('number');
|
||||
expect(typeof context!.establishedAt).toBe('number');
|
||||
expect(context!.source).toBe('manual');
|
||||
});
|
||||
|
||||
it('should create unique timestamps for different executions', async () => {
|
||||
const startNode = mock<INode>({ name: 'Start', type: 'n8n-nodes-base.start' });
|
||||
|
||||
const runExecutionData1: IRunExecutionData = {
|
||||
startData: {},
|
||||
resultData: { runData: {} },
|
||||
executionData: {
|
||||
contextData: {},
|
||||
nodeExecutionStack: [
|
||||
{
|
||||
node: startNode,
|
||||
data: { main: [[{ json: {} }]] },
|
||||
source: null,
|
||||
},
|
||||
],
|
||||
metadata: {},
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
};
|
||||
|
||||
const runExecutionData2: IRunExecutionData = {
|
||||
startData: {},
|
||||
resultData: { runData: {} },
|
||||
executionData: {
|
||||
contextData: {},
|
||||
nodeExecutionStack: [
|
||||
{
|
||||
node: startNode,
|
||||
data: { main: [[{ json: {} }]] },
|
||||
source: null,
|
||||
},
|
||||
],
|
||||
metadata: {},
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
};
|
||||
|
||||
await establishExecutionContext(
|
||||
mockWorkflow,
|
||||
runExecutionData1,
|
||||
mockAdditionalData,
|
||||
mockMode,
|
||||
);
|
||||
// Small delay to ensure different timestamps
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
await establishExecutionContext(
|
||||
mockWorkflow,
|
||||
runExecutionData2,
|
||||
mockAdditionalData,
|
||||
mockMode,
|
||||
);
|
||||
|
||||
const timestamp1 = runExecutionData1.executionData!.runtimeData!.establishedAt;
|
||||
const timestamp2 = runExecutionData2.executionData!.runtimeData!.establishedAt;
|
||||
|
||||
expect(timestamp1).toBeLessThan(timestamp2);
|
||||
});
|
||||
|
||||
it('should work with different execution modes', async () => {
|
||||
const startNode = mock<INode>({ name: 'Trigger', type: 'n8n-nodes-base.cron' });
|
||||
const modes: WorkflowExecuteMode[] = ['manual', 'trigger', 'webhook'];
|
||||
|
||||
for (const mode of modes) {
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
startData: {},
|
||||
resultData: { runData: {} },
|
||||
executionData: {
|
||||
contextData: {},
|
||||
nodeExecutionStack: [
|
||||
{
|
||||
node: startNode,
|
||||
data: { main: [[{ json: {} }]] },
|
||||
source: null,
|
||||
},
|
||||
],
|
||||
metadata: {},
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
};
|
||||
|
||||
await establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mode);
|
||||
|
||||
expect(runExecutionData.executionData!.runtimeData).toBeDefined();
|
||||
expect(runExecutionData.executionData!.runtimeData!.version).toBe(1);
|
||||
expect(runExecutionData.executionData!.runtimeData!.source).toBe(mode);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -122,6 +122,14 @@ describe('WorkflowExecute', () => {
|
|||
expect(result.finished).toEqual(true);
|
||||
expect(result.data.executionData!.contextData).toEqual({});
|
||||
expect(result.data.executionData!.nodeExecutionStack).toEqual([]);
|
||||
// Check if execution context was established
|
||||
expect(result.data.executionData!.runtimeData).toBeDefined();
|
||||
expect(result.data.executionData!.runtimeData).toHaveProperty('version', 1);
|
||||
expect(result.data.executionData!.runtimeData).toHaveProperty('establishedAt');
|
||||
expect(result.data.executionData!.runtimeData).toHaveProperty('source');
|
||||
expect(result.data.executionData!.runtimeData!.source).toEqual('manual');
|
||||
expect(typeof result.data.executionData!.runtimeData!.establishedAt).toBe('number');
|
||||
expect(result.data.executionData!.runtimeData!.establishedAt).toBeGreaterThan(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -191,6 +199,15 @@ describe('WorkflowExecute', () => {
|
|||
expect(result.finished).toEqual(true);
|
||||
expect(result.data.executionData!.contextData).toEqual({});
|
||||
expect(result.data.executionData!.nodeExecutionStack).toEqual([]);
|
||||
|
||||
// Check if execution context was established
|
||||
expect(result.data.executionData!.runtimeData).toBeDefined();
|
||||
expect(result.data.executionData!.runtimeData).toHaveProperty('version', 1);
|
||||
expect(result.data.executionData!.runtimeData).toHaveProperty('establishedAt');
|
||||
expect(result.data.executionData!.runtimeData).toHaveProperty('source');
|
||||
expect(result.data.executionData!.runtimeData!.source).toEqual('manual');
|
||||
expect(typeof result.data.executionData!.runtimeData!.establishedAt).toBe('number');
|
||||
expect(result.data.executionData!.runtimeData!.establishedAt).toBeGreaterThan(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -405,6 +422,15 @@ describe('WorkflowExecute', () => {
|
|||
expect(result.finished).toEqual(true);
|
||||
// expect(result.data.executionData!.contextData).toEqual({}); //Fails when test workflow Includes splitInbatches
|
||||
expect(result.data.executionData!.nodeExecutionStack).toEqual([]);
|
||||
|
||||
// Check if execution context was established
|
||||
expect(result.data.executionData!.runtimeData).toBeDefined();
|
||||
expect(result.data.executionData!.runtimeData).toHaveProperty('version', 1);
|
||||
expect(result.data.executionData!.runtimeData).toHaveProperty('establishedAt');
|
||||
expect(result.data.executionData!.runtimeData).toHaveProperty('source');
|
||||
expect(result.data.executionData!.runtimeData!.source).toEqual('manual');
|
||||
expect(typeof result.data.executionData!.runtimeData!.establishedAt).toBe('number');
|
||||
expect(result.data.executionData!.runtimeData!.establishedAt).toBeGreaterThan(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
119
packages/core/src/execution-engine/execution-context.ts
Normal file
119
packages/core/src/execution-engine/execution-context.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import {
|
||||
type IWorkflowExecuteAdditionalData,
|
||||
type WorkflowExecuteMode,
|
||||
type IRunExecutionData,
|
||||
type Workflow,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { assertExecutionDataExists } from '@/utils/assertions';
|
||||
|
||||
/**
|
||||
* Establishes the execution context for a workflow run.
|
||||
*
|
||||
* This function creates and initializes the execution context that persists throughout
|
||||
* the workflow execution lifecycle. The context is stored directly in the provided
|
||||
* `runExecutionData.executionData.runtimeData` object.
|
||||
*
|
||||
* @param workflow - The workflow instance being executed (reserved for future context extraction)
|
||||
* @param runExecutionData - The execution data structure that will be mutated to include the execution context
|
||||
* @param additionalData - Additional workflow execution data used for validation and future context extraction
|
||||
* @param mode - The workflow execution mode (manual, trigger, webhook, etc.)
|
||||
*
|
||||
* @returns Promise that resolves when context has been established
|
||||
*
|
||||
* @throws {UnexpectedError} When `runExecutionData.executionData` is missing or invalid
|
||||
*
|
||||
* @remarks
|
||||
* ## Mutation Behavior
|
||||
* This function mutates the provided `runExecutionData` object by setting:
|
||||
* - `runExecutionData.executionData.runtimeData` with the newly created execution context
|
||||
*
|
||||
* ## Context Creation
|
||||
* - Creates a new context with version 1 schema and current timestamp
|
||||
* - Establishes basic context for all workflows (including Chat Trigger workflows)
|
||||
* - Supports future extraction of context information from start node (when available)
|
||||
* - Validates execution data structure before context creation
|
||||
*
|
||||
* ## Chat Trigger Support
|
||||
* Workflows containing only Chat Trigger nodes have an empty `nodeExecutionStack`.
|
||||
* In such cases, basic context (version + timestamp) is still established, but no
|
||||
* start-node-specific context extraction is performed.
|
||||
*
|
||||
* ## Future Enhancements
|
||||
* The function is designed to support extracting context information from:
|
||||
* - Start node parameters (e.g., webhook authentication tokens)
|
||||
* - Start node type (trigger, manual, webhook, etc.)
|
||||
* - Input data from triggering events
|
||||
* - User identification from various sources
|
||||
*
|
||||
* ## Example Usage
|
||||
* ```typescript
|
||||
* await establishExecutionContext(workflow, runExecutionData, additionalData, mode);
|
||||
* // Context is now available in: runExecutionData.executionData.runtimeData
|
||||
* ```
|
||||
*
|
||||
* @see IExecutionContextV1 for context structure definition
|
||||
* @see IRunExecutionData for execution data structure
|
||||
* @see IWorkflowExecuteAdditionalData for additional execution data
|
||||
*/
|
||||
export const establishExecutionContext = async (
|
||||
workflow: Workflow,
|
||||
runExecutionData: IRunExecutionData,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
mode: WorkflowExecuteMode,
|
||||
): Promise<void> => {
|
||||
assertExecutionDataExists(runExecutionData.executionData, workflow, additionalData, mode);
|
||||
|
||||
const executionData = runExecutionData.executionData;
|
||||
|
||||
// At this point we have established the basic execution context.
|
||||
// If a context is already established we overwrite it.
|
||||
// This might change depending on the propagation strategy we want to implement in the future.
|
||||
executionData.runtimeData = {
|
||||
version: 1,
|
||||
establishedAt: Date.now(),
|
||||
source: mode,
|
||||
};
|
||||
|
||||
// Next, we attempt to extract additional context from the start node of the execution stack.
|
||||
const [startItem] = executionData.nodeExecutionStack;
|
||||
|
||||
// The nodeExecutionStack is typically initialized in one of three ways:
|
||||
// 1. run() method: Creates stack with start node (workflow-execute.ts:143-157)
|
||||
// 2. runPartialWorkflow2(): Recreates stack from existing runData via recreateNodeExecutionStack()
|
||||
// 3. Constructor with executionData: Pre-populated from caller (resume scenarios)
|
||||
//
|
||||
// However, the stack CAN be legitimately empty for workflows containing only Chat Trigger nodes
|
||||
// (see workflow-execute.ts:1368-1369). In such cases, we cannot extract context from a start
|
||||
// node, but we should still establish basic execution context.
|
||||
//
|
||||
// We cannot extract user specific information from the initial item though. So we exit early.
|
||||
if (!startItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: the following comments will be implemented in future iterations
|
||||
// to extract more context information based on the start node
|
||||
// and the data that triggered the workflow execution.
|
||||
// the comments are left here for now to provide more context on how
|
||||
// this can be achieved.
|
||||
|
||||
// startNodeParameters will hold the parameters of the start node
|
||||
// this can be the settings for the different hooks to be executed
|
||||
// for example to extract the bearer token from the start node data.
|
||||
|
||||
// const startNodeParameters = startItem.node.parameters;
|
||||
|
||||
// startNodeType holds the type of the start node
|
||||
|
||||
// const startNodeType = startItem.node.type;
|
||||
|
||||
// Main input data is an array of items, each item represents an event that triggers the workflow execution
|
||||
// The 'main' selector selects the input name of the nodes, and the 0 index represents the runIndex,
|
||||
// 0 being the first run of this node in the workflow.
|
||||
|
||||
// const mainInput = startItem.data["main"][0];
|
||||
|
||||
// based on startNodeParameters, startNodeType and mainInput we can now
|
||||
// iterate over the different hooks to extract specific data for the runtime context
|
||||
};
|
||||
|
|
@ -59,8 +59,10 @@ import PCancelable from 'p-cancelable';
|
|||
import { ErrorReporter } from '@/errors/error-reporter';
|
||||
import { WorkflowHasIssuesError } from '@/errors/workflow-has-issues.error';
|
||||
import * as NodeExecuteFunctions from '@/node-execute-functions';
|
||||
import { assertExecutionDataExists } from '@/utils/assertions';
|
||||
import { isJsonCompatible } from '@/utils/is-json-compatible';
|
||||
|
||||
import { establishExecutionContext } from './execution-context';
|
||||
import type { ExecutionLifecycleHooks } from './execution-lifecycle-hooks';
|
||||
import { ExecuteContext, PollContext } from './node-execution-context';
|
||||
import {
|
||||
|
|
@ -1325,22 +1327,6 @@ export class WorkflowExecute {
|
|||
);
|
||||
}
|
||||
|
||||
private assertExecutionDataExists(
|
||||
this: WorkflowExecute,
|
||||
executionData: IRunExecutionData['executionData'],
|
||||
workflow: Workflow,
|
||||
): asserts executionData is NonNullable<IRunExecutionData['executionData']> {
|
||||
if (!executionData) {
|
||||
throw new UnexpectedError('Failed to run workflow due to missing execution data', {
|
||||
extra: {
|
||||
workflowId: workflow.id,
|
||||
executionId: this.additionalData.executionId,
|
||||
mode: this.mode,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles executions that have been waiting by
|
||||
* 1. unsetting the `waitTill`
|
||||
|
|
@ -1354,7 +1340,12 @@ export class WorkflowExecute {
|
|||
if (this.runExecutionData.waitTill) {
|
||||
this.runExecutionData.waitTill = undefined;
|
||||
|
||||
this.assertExecutionDataExists(this.runExecutionData.executionData, workflow);
|
||||
assertExecutionDataExists(
|
||||
this.runExecutionData.executionData,
|
||||
workflow,
|
||||
this.additionalData,
|
||||
this.mode,
|
||||
);
|
||||
this.runExecutionData.executionData.nodeExecutionStack[0].node.disabled = true;
|
||||
|
||||
const lastNodeExecuted = this.runExecutionData.resultData.lastNodeExecuted as string;
|
||||
|
|
@ -1363,7 +1354,12 @@ export class WorkflowExecute {
|
|||
}
|
||||
|
||||
private checkForWorkflowIssues(workflow: Workflow): void {
|
||||
this.assertExecutionDataExists(this.runExecutionData.executionData, workflow);
|
||||
assertExecutionDataExists(
|
||||
this.runExecutionData.executionData,
|
||||
workflow,
|
||||
this.additionalData,
|
||||
this.mode,
|
||||
);
|
||||
// Node execution stack will be empty for an execution containing only Chat
|
||||
// Trigger.
|
||||
const startNode = this.runExecutionData.executionData.nodeExecutionStack.at(0)?.node.name;
|
||||
|
|
@ -1487,6 +1483,14 @@ export class WorkflowExecute {
|
|||
// eslint-disable-next-line complexity
|
||||
const returnPromise = (async () => {
|
||||
try {
|
||||
// Establish the execution context
|
||||
await establishExecutionContext(
|
||||
workflow,
|
||||
this.runExecutionData,
|
||||
this.additionalData,
|
||||
this.mode,
|
||||
);
|
||||
|
||||
if (!this.additionalData.restartExecutionId) {
|
||||
await hooks.runHook('workflowExecuteBefore', [workflow, this.runExecutionData]);
|
||||
}
|
||||
|
|
|
|||
109
packages/core/src/utils/__tests__/assertions.test.ts
Normal file
109
packages/core/src/utils/__tests__/assertions.test.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { mock } from 'jest-mock-extended';
|
||||
import type { IRunExecutionData, IWorkflowExecuteAdditionalData, Workflow } from 'n8n-workflow';
|
||||
import { UnexpectedError } from 'n8n-workflow';
|
||||
|
||||
import { assertExecutionDataExists } from '../assertions';
|
||||
|
||||
describe('assertExecutionDataExists', () => {
|
||||
const mockWorkflow = mock<Workflow>({ id: 'test-workflow-123' });
|
||||
const mockAdditionalData = mock<IWorkflowExecuteAdditionalData>({
|
||||
executionId: 'test-execution-456',
|
||||
});
|
||||
const mode = 'manual';
|
||||
|
||||
describe('when executionData is valid', () => {
|
||||
it('should not throw when executionData exists', () => {
|
||||
const validExecutionData = {
|
||||
contextData: {},
|
||||
nodeExecutionStack: [],
|
||||
metadata: {},
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
assertExecutionDataExists(validExecutionData, mockWorkflow, mockAdditionalData, mode),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw when executionData has minimal properties', () => {
|
||||
const minimalExecutionData = {
|
||||
nodeExecutionStack: [],
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
assertExecutionDataExists(
|
||||
minimalExecutionData as unknown as IRunExecutionData['executionData'],
|
||||
mockWorkflow,
|
||||
mockAdditionalData,
|
||||
mode,
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when executionData is missing', () => {
|
||||
it('should throw UnexpectedError when executionData is undefined', () => {
|
||||
expect(() =>
|
||||
assertExecutionDataExists(undefined, mockWorkflow, mockAdditionalData, mode),
|
||||
).toThrow(UnexpectedError);
|
||||
});
|
||||
|
||||
it('should throw UnexpectedError when executionData is null', () => {
|
||||
expect(() =>
|
||||
assertExecutionDataExists(
|
||||
null as unknown as IRunExecutionData['executionData'],
|
||||
mockWorkflow,
|
||||
mockAdditionalData,
|
||||
mode,
|
||||
),
|
||||
).toThrow(UnexpectedError);
|
||||
});
|
||||
|
||||
it('should throw error with correct message', () => {
|
||||
expect(() =>
|
||||
assertExecutionDataExists(undefined, mockWorkflow, mockAdditionalData, mode),
|
||||
).toThrow(/missing execution data/i);
|
||||
});
|
||||
|
||||
it('should include workflow metadata in error', () => {
|
||||
try {
|
||||
assertExecutionDataExists(undefined, mockWorkflow, mockAdditionalData, mode);
|
||||
fail('Expected error to be thrown');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(UnexpectedError);
|
||||
const unexpectedError = error as UnexpectedError;
|
||||
expect(unexpectedError.extra).toEqual({
|
||||
workflowId: 'test-workflow-123',
|
||||
executionId: 'test-execution-456',
|
||||
mode: 'manual',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('with different execution modes', () => {
|
||||
it('should work with trigger mode', () => {
|
||||
const validExecutionData = { nodeExecutionStack: [] };
|
||||
|
||||
expect(() =>
|
||||
assertExecutionDataExists(
|
||||
validExecutionData as unknown as IRunExecutionData['executionData'],
|
||||
mockWorkflow,
|
||||
mockAdditionalData,
|
||||
'trigger',
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should include mode in error metadata', () => {
|
||||
try {
|
||||
assertExecutionDataExists(undefined, mockWorkflow, mockAdditionalData, 'webhook');
|
||||
fail('Expected error to be thrown');
|
||||
} catch (error) {
|
||||
const unexpectedError = error as UnexpectedError;
|
||||
expect(unexpectedError.extra?.mode).toBe('webhook');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
24
packages/core/src/utils/assertions.ts
Normal file
24
packages/core/src/utils/assertions.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import {
|
||||
type IRunExecutionData,
|
||||
type IWorkflowExecuteAdditionalData,
|
||||
UnexpectedError,
|
||||
type Workflow,
|
||||
type WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export function assertExecutionDataExists(
|
||||
executionData: IRunExecutionData['executionData'],
|
||||
workflow: Workflow,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
mode: WorkflowExecuteMode,
|
||||
): asserts executionData is NonNullable<IRunExecutionData['executionData']> {
|
||||
if (!executionData) {
|
||||
throw new UnexpectedError('Failed to run workflow due to missing execution data', {
|
||||
extra: {
|
||||
workflowId: workflow.id,
|
||||
executionId: additionalData.executionId,
|
||||
mode,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -30,12 +30,33 @@ export const CredentialContextSchema = z
|
|||
*/
|
||||
export type ICredentialContext = z.output<typeof CredentialContextSchema>;
|
||||
|
||||
const WorkflowExecuteModeSchema = z.union([
|
||||
z.literal('cli'),
|
||||
z.literal('error'),
|
||||
z.literal('integrated'),
|
||||
z.literal('internal'),
|
||||
z.literal('manual'),
|
||||
z.literal('retry'),
|
||||
z.literal('trigger'),
|
||||
z.literal('webhook'),
|
||||
z.literal('evaluation'),
|
||||
z.literal('chat'),
|
||||
]);
|
||||
|
||||
export type WorkflowExecuteModeValues = z.infer<typeof WorkflowExecuteModeSchema>;
|
||||
|
||||
const ExecutionContextSchemaV1 = z.object({
|
||||
version: z.literal(1),
|
||||
/**
|
||||
* When the context was established (Unix timestamp in milliseconds)
|
||||
*/
|
||||
establishedAt: z.number(),
|
||||
|
||||
/**
|
||||
* The mode in which the workflow is being executed
|
||||
*/
|
||||
source: WorkflowExecuteModeSchema,
|
||||
|
||||
/**
|
||||
* Encrypted credential context for dynamic credential resolution
|
||||
* Always encrypted when stored, decrypted on-demand by credential resolver
|
||||
|
|
|
|||
|
|
@ -24,11 +24,16 @@ import type { NodeApiError } from './errors/node-api.error';
|
|||
import type { NodeOperationError } from './errors/node-operation.error';
|
||||
import type { WorkflowActivationError } from './errors/workflow-activation.error';
|
||||
import type { WorkflowOperationError } from './errors/workflow-operation.error';
|
||||
import type {
|
||||
IExecutionContext,
|
||||
WorkflowExecuteModeValues as WorkflowExecuteMode,
|
||||
} from './execution-context';
|
||||
import type { ExecutionStatus } from './execution-status';
|
||||
import type { Result } from './result';
|
||||
import type { Workflow } from './workflow';
|
||||
import type { EnvProviderState } from './workflow-data-proxy-env-provider';
|
||||
import type { IExecutionContext } from './execution-context';
|
||||
|
||||
export type { WorkflowExecuteModeValues as WorkflowExecuteMode } from './execution-context';
|
||||
|
||||
export interface IAdditionalCredentialOptions {
|
||||
oauth2?: IOAuth2Options;
|
||||
|
|
@ -2723,18 +2728,6 @@ export interface IWorkflowExecuteAdditionalData {
|
|||
): Promise<Result<T, E>>;
|
||||
}
|
||||
|
||||
export type WorkflowExecuteMode =
|
||||
| 'cli'
|
||||
| 'error'
|
||||
| 'integrated'
|
||||
| 'internal'
|
||||
| 'manual'
|
||||
| 'retry'
|
||||
| 'trigger'
|
||||
| 'webhook'
|
||||
| 'evaluation'
|
||||
| 'chat';
|
||||
|
||||
export type WorkflowActivateMode =
|
||||
| 'init'
|
||||
| 'create' // unused
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user