import get from 'lodash/get'; import type { Workflow, INode, IWorkflowExecuteAdditionalData, WorkflowExecuteMode, IRunExecutionData, INodeExecutionData, ITaskDataConnections, IExecuteData, ICredentialDataDecryptedObject, CallbackManager, IExecuteWorkflowInfo, RelatedExecution, ExecuteWorkflowData, ITaskMetadata, ContextType, IContextObject, IWorkflowDataProxyData, ISourceData, AiEvent, NodeConnectionType, Result, IExecuteFunctions, } from 'n8n-workflow'; import { ApplicationError, NodeHelpers, NodeConnectionTypes, WAIT_INDEFINITELY, WorkflowDataProxy, createEnvProviderState, } from 'n8n-workflow'; import { NodeExecutionContext } from './node-execution-context'; export class BaseExecuteContext extends NodeExecutionContext { constructor( workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, readonly runExecutionData: IRunExecutionData, runIndex: number, readonly connectionInputData: INodeExecutionData[], readonly inputData: ITaskDataConnections, readonly executeData: IExecuteData, readonly abortSignal?: AbortSignal, ) { super(workflow, node, additionalData, mode, runExecutionData, runIndex); } getExecutionContext() { return this.runExecutionData.executionData?.runtimeData; } getExecutionCancelSignal() { return this.abortSignal; } onExecutionCancellation(handler: () => unknown) { const fn = () => { this.abortSignal?.removeEventListener('abort', fn); handler(); }; this.abortSignal?.addEventListener('abort', fn); } getExecuteData() { return this.executeData; } setMetadata(metadata: ITaskMetadata): void { this.executeData.metadata = { ...(this.executeData.metadata ?? {}), ...metadata, }; } getContext(type: ContextType): IContextObject { return NodeHelpers.getContext(this.runExecutionData, type, this.node); } /** Returns if execution should be continued even if there was an error */ continueOnFail(): boolean { const onError = get(this.node, 'onError', undefined); if (onError === undefined) { return get(this.node, 'continueOnFail', false); } return ['continueRegularOutput', 'continueErrorOutput'].includes(onError); } async getCredentials( type: string, itemIndex: number, ) { return await this._getCredentials( type, this.executeData, this.connectionInputData, itemIndex, ); } async putExecutionToWait(waitTill: Date): Promise { this.runExecutionData.waitTill = waitTill; if (this.additionalData.setExecutionStatus) { this.additionalData.setExecutionStatus('waiting'); } } async executeWorkflow( workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[], parentCallbackManager?: CallbackManager, options?: { doNotWaitToFinish?: boolean; parentExecution?: RelatedExecution; executionMode?: WorkflowExecuteMode; }, ): Promise { if (options?.parentExecution) { // We inject the execution context of the current execution // to the sub-workflow so that it can be accessed there // this should only happen for the direct parent execution // if a workflow starts a sub-workflow for a workflow that is not itself // then the context should not be passed down if ( !options.parentExecution.executionContext && options.parentExecution.executionId === this.getExecutionId() ) { options.parentExecution.executionContext = this.getExecutionContext(); } } const result = await this.additionalData.executeWorkflow(workflowInfo, this.additionalData, { ...options, parentWorkflowId: this.workflow.id, inputData, parentWorkflowSettings: this.workflow.settings, node: this.node, parentCallbackManager, }); // If a sub-workflow execution goes into the waiting state if (result.waitTill) { // then put the parent workflow execution also into the waiting state, // but do not use the sub-workflow `waitTill` to avoid WaitTracker resuming the parent execution at the same time as the sub-workflow await this.putExecutionToWait(WAIT_INDEFINITELY); } return result; } async getExecutionDataById(executionId: string): Promise { return await this.additionalData.getRunExecutionData(executionId); } protected getInputItems(inputIndex: number, connectionType: NodeConnectionType) { const inputData = this.inputData[connectionType]; if (inputData.length < inputIndex) { throw new ApplicationError('Could not get input with given index', { extra: { inputIndex, connectionType }, }); } const allItems = inputData[inputIndex] as INodeExecutionData[] | null | undefined; if (allItems === null) { throw new ApplicationError('Input index was not set', { extra: { inputIndex, connectionType }, }); } return allItems; } getInputSourceData(inputIndex = 0, connectionType = NodeConnectionTypes.Main): ISourceData { if (this.executeData?.source === null) { // Should never happen as n8n sets it automatically throw new ApplicationError('Source data is missing'); } return this.executeData.source[connectionType][inputIndex]!; } getWorkflowDataProxy(itemIndex: number): IWorkflowDataProxyData { return new WorkflowDataProxy( this.workflow, this.runExecutionData, this.runIndex, itemIndex, this.node.name, this.connectionInputData, {}, this.mode, this.additionalKeys, this.executeData, ).getDataProxy(); } // eslint-disable-next-line @typescript-eslint/no-explicit-any sendMessageToUI(...args: any[]): void { if (this.mode !== 'manual') { return; } try { if (this.additionalData.sendDataToUI) { args = args.map((arg) => { // prevent invalid dates from being logged as null // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return if (arg.isLuxonDateTime && arg.invalidReason) return { ...arg }; // log valid dates in human readable format, as in browser // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument if (arg.isLuxonDateTime) return new Date(arg.ts).toString(); if (arg instanceof Date) return arg.toString(); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return arg; }); this.additionalData.sendDataToUI('sendConsoleMessage', { source: `[Node: "${this.node.name}"]`, messages: args, }); } } catch (error) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access this.logger.warn(`There was a problem sending message to UI: ${error.message}`); } } logAiEvent(eventName: AiEvent, msg: string) { return this.additionalData.logAiEvent(eventName, { executionId: this.additionalData.executionId ?? 'unsaved-execution', nodeName: this.node.name, workflowName: this.workflow.name ?? 'Unnamed workflow', nodeType: this.node.type, workflowId: this.workflow.id ?? 'unsaved-workflow', msg, }); } async startJob( jobType: string, settings: unknown, itemIndex: number, ): Promise> { return await this.additionalData.startRunnerTask( this.additionalData, jobType, settings, this as IExecuteFunctions, this.inputData, this.node, this.workflow, this.runExecutionData, this.runIndex, itemIndex, this.node.name, this.connectionInputData, {}, this.mode, createEnvProviderState(), this.executeData, ); } getRunnerStatus(taskType: string): { available: true } | { available: false; reason?: string } { return this.additionalData.getRunnerStatus?.(taskType) ?? { available: true }; } }