From 1078fa662a9171a3e79233bbf48ac62e7e062783 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:01:38 +0300 Subject: [PATCH] fix: Make sure errors are transferred correctly from js task runner (no-changelog) (#11214) --- .../__tests__/js-task-runner.test.ts | 59 +++++++++- .../errors/__tests__/execution-error.test.ts | 53 +++++++++ .../src/js-task-runner/errors/error-like.ts | 12 +++ .../js-task-runner/errors/execution-error.ts | 24 +++-- .../errors/serializable-error.ts | 21 ++++ .../js-task-runner/errors/validation-error.ts | 4 +- .../src/js-task-runner/js-task-runner.ts | 34 +++--- packages/@n8n/task-runner/src/task-runner.ts | 5 +- .../src/runners/task-managers/task-manager.ts | 49 ++++----- packages/core/src/Agent/index.ts | 7 +- .../nodes/Code/JsTaskRunnerSandbox.ts | 101 +++++++----------- .../Code/errors/WrappedExecutionError.ts | 35 ++++++ packages/workflow/src/Interfaces.ts | 13 ++- .../src/NodeParameters/FilterParameter.ts | 2 +- packages/workflow/src/index.ts | 1 + packages/workflow/src/result.ts | 13 +++ 16 files changed, 311 insertions(+), 122 deletions(-) create mode 100644 packages/@n8n/task-runner/src/js-task-runner/errors/__tests__/execution-error.test.ts create mode 100644 packages/@n8n/task-runner/src/js-task-runner/errors/error-like.ts create mode 100644 packages/@n8n/task-runner/src/js-task-runner/errors/serializable-error.ts create mode 100644 packages/nodes-base/nodes/Code/errors/WrappedExecutionError.ts create mode 100644 packages/workflow/src/result.ts diff --git a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts index c940966fbd..499105f39d 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts @@ -13,6 +13,7 @@ import { import type { Task } from '@/task-runner'; import { newAllCodeTaskData, newTaskWithSettings, withPairedItem, wrapIntoJson } from './test-data'; +import { ExecutionError } from '../errors/execution-error'; jest.mock('ws'); @@ -292,7 +293,7 @@ describe('JsTaskRunner', () => { }); expect(outcome).toEqual({ - result: [wrapIntoJson({ error: 'Error message' })], + result: [wrapIntoJson({ error: 'Error message [line 1]' })], customData: undefined, }); }); @@ -406,8 +407,8 @@ describe('JsTaskRunner', () => { expect(outcome).toEqual({ result: [ - withPairedItem(0, wrapIntoJson({ error: 'Error message' })), - withPairedItem(1, wrapIntoJson({ error: 'Error message' })), + withPairedItem(0, wrapIntoJson({ error: 'Error message [line 1]' })), + withPairedItem(1, wrapIntoJson({ error: 'Error message [line 1]' })), ], customData: undefined, }); @@ -706,4 +707,56 @@ describe('JsTaskRunner', () => { ); }); }); + + describe('errors', () => { + test.each<[CodeExecutionMode]>([['runOnceForAllItems'], ['runOnceForEachItem']])( + 'should throw an ExecutionError if the code is invalid in %s mode', + async (nodeMode) => { + await expect( + execTaskWithParams({ + task: newTaskWithSettings({ + code: 'unknown', + nodeMode, + }), + taskData: newAllCodeTaskData([wrapIntoJson({ a: 1 })]), + }), + ).rejects.toThrow(ExecutionError); + }, + ); + + it('sends serializes an error correctly', async () => { + const runner = createRunnerWithOpts({}); + const taskId = '1'; + const task = newTaskWithSettings({ + code: 'unknown; return []', + nodeMode: 'runOnceForAllItems', + continueOnFail: false, + mode: 'manual', + workflowMode: 'manual', + }); + runner.runningTasks.set(taskId, task); + + const sendSpy = jest.spyOn(runner.ws, 'send').mockImplementation(() => {}); + jest.spyOn(runner, 'sendOffers').mockImplementation(() => {}); + jest + .spyOn(runner, 'requestData') + .mockResolvedValue(newAllCodeTaskData([wrapIntoJson({ a: 1 })])); + + await runner.receivedSettings(taskId, task.settings); + + expect(sendSpy).toHaveBeenCalledWith( + JSON.stringify({ + type: 'runner:taskerror', + taskId, + error: { + message: 'unknown is not defined [line 1]', + description: 'ReferenceError', + lineNumber: 1, + }, + }), + ); + + console.log('DONE'); + }, 1000); + }); }); diff --git a/packages/@n8n/task-runner/src/js-task-runner/errors/__tests__/execution-error.test.ts b/packages/@n8n/task-runner/src/js-task-runner/errors/__tests__/execution-error.test.ts new file mode 100644 index 0000000000..3777940021 --- /dev/null +++ b/packages/@n8n/task-runner/src/js-task-runner/errors/__tests__/execution-error.test.ts @@ -0,0 +1,53 @@ +import { ExecutionError } from '../execution-error'; + +describe('ExecutionError', () => { + const defaultStack = `TypeError: a.unknown is not a function + at VmCodeWrapper (evalmachine.:2:3) + at evalmachine.:7:2 + at Script.runInContext (node:vm:148:12) + at Script.runInNewContext (node:vm:153:17) + at runInNewContext (node:vm:309:38) + at JsTaskRunner.runForAllItems (/n8n/packages/@n8n/task-runner/dist/js-task-runner/js-task-runner.js:90:65) + at JsTaskRunner.executeTask (/n8n/packages/@n8n/task-runner/dist/js-task-runner/js-task-runner.js:71:26) + at process.processTicksAndRejections (node:internal/process/task_queues:95:5) + at async JsTaskRunner.receivedSettings (/n8n/packages/@n8n/task-runner/dist/task-runner.js:190:26)`; + + it('should parse error details from stack trace without itemIndex', () => { + const error = new Error('a.unknown is not a function'); + error.stack = defaultStack; + + const executionError = new ExecutionError(error); + expect(executionError.message).toBe('a.unknown is not a function [line 2]'); + expect(executionError.lineNumber).toBe(2); + expect(executionError.description).toBe('TypeError'); + expect(executionError.context).toBeUndefined(); + }); + + it('should parse error details from stack trace with itemIndex', () => { + const error = new Error('a.unknown is not a function'); + error.stack = defaultStack; + + const executionError = new ExecutionError(error, 1); + expect(executionError.message).toBe('a.unknown is not a function [line 2, for item 1]'); + expect(executionError.lineNumber).toBe(2); + expect(executionError.description).toBe('TypeError'); + expect(executionError.context).toEqual({ itemIndex: 1 }); + }); + + it('should serialize correctly', () => { + const error = new Error('a.unknown is not a function'); + error.stack = defaultStack; + + const executionError = new ExecutionError(error, 1); + + expect(JSON.stringify(executionError)).toBe( + JSON.stringify({ + message: 'a.unknown is not a function [line 2, for item 1]', + description: 'TypeError', + itemIndex: 1, + context: { itemIndex: 1 }, + lineNumber: 2, + }), + ); + }); +}); diff --git a/packages/@n8n/task-runner/src/js-task-runner/errors/error-like.ts b/packages/@n8n/task-runner/src/js-task-runner/errors/error-like.ts new file mode 100644 index 0000000000..1eaf744e89 --- /dev/null +++ b/packages/@n8n/task-runner/src/js-task-runner/errors/error-like.ts @@ -0,0 +1,12 @@ +export interface ErrorLike { + message: string; + stack?: string; +} + +export function isErrorLike(value: unknown): value is ErrorLike { + if (typeof value !== 'object' || value === null) return false; + + const errorLike = value as ErrorLike; + + return typeof errorLike.message === 'string'; +} diff --git a/packages/@n8n/task-runner/src/js-task-runner/errors/execution-error.ts b/packages/@n8n/task-runner/src/js-task-runner/errors/execution-error.ts index e1fdffc0b6..63a2dd5e0b 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/errors/execution-error.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/errors/execution-error.ts @@ -1,6 +1,9 @@ -import { ApplicationError } from 'n8n-workflow'; +import type { ErrorLike } from './error-like'; +import { SerializableError } from './serializable-error'; -export class ExecutionError extends ApplicationError { +const VM_WRAPPER_FN_NAME = 'VmCodeWrapper'; + +export class ExecutionError extends SerializableError { description: string | null = null; itemIndex: number | undefined = undefined; @@ -11,7 +14,7 @@ export class ExecutionError extends ApplicationError { lineNumber: number | undefined = undefined; - constructor(error: Error & { stack?: string }, itemIndex?: number) { + constructor(error: ErrorLike, itemIndex?: number) { super(error.message); this.itemIndex = itemIndex; @@ -32,10 +35,11 @@ export class ExecutionError extends ApplicationError { if (stackRows.length === 0) { this.message = 'Unknown error'; + return; } const messageRow = stackRows.find((line) => line.includes('Error:')); - const lineNumberRow = stackRows.find((line) => line.includes('Code:')); + const lineNumberRow = stackRows.find((line) => line.includes(`at ${VM_WRAPPER_FN_NAME} `)); const lineNumberDisplay = this.toLineNumberDisplay(lineNumberRow); if (!messageRow) { @@ -56,16 +60,22 @@ export class ExecutionError extends ApplicationError { } private toLineNumberDisplay(lineNumberRow?: string) { - const errorLineNumberMatch = lineNumberRow?.match(/Code:(?\d+)/); + if (!lineNumberRow) return ''; + // TODO: This doesn't work if there is a function definition in the code + // and the error is thrown from that function. + + const regex = new RegExp( + `at ${VM_WRAPPER_FN_NAME} \\(evalmachine\\.:(?\\d+):`, + ); + const errorLineNumberMatch = lineNumberRow.match(regex); if (!errorLineNumberMatch?.groups?.lineNumber) return null; const lineNumber = errorLineNumberMatch.groups.lineNumber; + if (!lineNumber) return ''; this.lineNumber = Number(lineNumber); - if (!lineNumber) return ''; - return this.itemIndex === undefined ? `[line ${lineNumber}]` : `[line ${lineNumber}, for item ${this.itemIndex}]`; diff --git a/packages/@n8n/task-runner/src/js-task-runner/errors/serializable-error.ts b/packages/@n8n/task-runner/src/js-task-runner/errors/serializable-error.ts new file mode 100644 index 0000000000..cd0e568de0 --- /dev/null +++ b/packages/@n8n/task-runner/src/js-task-runner/errors/serializable-error.ts @@ -0,0 +1,21 @@ +/** + * Error that has its message property serialized as well. Used to transport + * errors over the wire. + */ +export abstract class SerializableError extends Error { + constructor(message: string) { + super(message); + + // So it is serialized as well + this.makeMessageEnumerable(); + } + + private makeMessageEnumerable() { + Object.defineProperty(this, 'message', { + value: this.message, + enumerable: true, // This makes the message property enumerable + writable: true, + configurable: true, + }); + } +} diff --git a/packages/@n8n/task-runner/src/js-task-runner/errors/validation-error.ts b/packages/@n8n/task-runner/src/js-task-runner/errors/validation-error.ts index f2ba712c2c..bf66136ccf 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/errors/validation-error.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/errors/validation-error.ts @@ -1,6 +1,6 @@ -import { ApplicationError } from 'n8n-workflow'; +import { SerializableError } from './serializable-error'; -export class ValidationError extends ApplicationError { +export class ValidationError extends SerializableError { description = ''; itemIndex: number | undefined = undefined; diff --git a/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts b/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts index 4b861fd3cc..5bf2e06f26 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts @@ -25,6 +25,8 @@ import { runInNewContext, type Context } from 'node:vm'; import type { TaskResultData } from '@/runner-types'; import { type Task, TaskRunner } from '@/task-runner'; +import { isErrorLike } from './errors/error-like'; +import { ExecutionError } from './errors/execution-error'; import type { RequireResolver } from './require-resolver'; import { createRequireResolver } from './require-resolver'; import { validateRunForAllItemsOutput, validateRunForEachItemOutput } from './result-validation'; @@ -186,7 +188,7 @@ export class JsTaskRunner extends TaskRunner { try { const result = (await runInNewContext( - `module.exports = async function() {${settings.code}\n}()`, + `module.exports = async function VmCodeWrapper() {${settings.code}\n}()`, context, )) as TaskResultData['result']; @@ -195,12 +197,14 @@ export class JsTaskRunner extends TaskRunner { } return validateRunForAllItemsOutput(result); - } catch (error) { + } catch (e) { + // Errors thrown by the VM are not instances of Error, so map them to an ExecutionError + const error = this.toExecutionErrorIfNeeded(e); + if (settings.continueOnFail) { - return [{ json: { error: this.getErrorMessageFromVmError(error) } }]; + return [{ json: { error: error.message } }]; } - (error as Record).node = allData.node; throw error; } } @@ -233,7 +237,7 @@ export class JsTaskRunner extends TaskRunner { try { let result = (await runInNewContext( - `module.exports = async function() {${settings.code}\n}()`, + `module.exports = async function VmCodeWrapper() {${settings.code}\n}()`, context, )) as INodeExecutionData | undefined; @@ -257,14 +261,16 @@ export class JsTaskRunner extends TaskRunner { }, ); } - } catch (error) { + } catch (e) { + // Errors thrown by the VM are not instances of Error, so map them to an ExecutionError + const error = this.toExecutionErrorIfNeeded(e); + if (!settings.continueOnFail) { - (error as Record).node = allData.node; throw error; } returnData.push({ - json: { error: this.getErrorMessageFromVmError(error) }, + json: { error: error.message }, pairedItem: { item: index, }, @@ -304,11 +310,15 @@ export class JsTaskRunner extends TaskRunner { ).getDataProxy(); } - private getErrorMessageFromVmError(error: unknown): string { - if (typeof error === 'object' && !!error && 'message' in error) { - return error.message as string; + private toExecutionErrorIfNeeded(error: unknown): Error { + if (error instanceof Error) { + return error; } - return JSON.stringify(error); + if (isErrorLike(error)) { + return new ExecutionError(error); + } + + return new ExecutionError({ message: JSON.stringify(error) }); } } diff --git a/packages/@n8n/task-runner/src/task-runner.ts b/packages/@n8n/task-runner/src/task-runner.ts index 89402d885c..ac8378636a 100644 --- a/packages/@n8n/task-runner/src/task-runner.ts +++ b/packages/@n8n/task-runner/src/task-runner.ts @@ -1,4 +1,4 @@ -import { ApplicationError, ensureError } from 'n8n-workflow'; +import { ApplicationError } from 'n8n-workflow'; import { nanoid } from 'nanoid'; import { URL } from 'node:url'; import { type MessageEvent, WebSocket } from 'ws'; @@ -256,8 +256,7 @@ export abstract class TaskRunner { try { const data = await this.executeTask(task); this.taskDone(taskId, data); - } catch (e) { - const error = ensureError(e); + } catch (error) { this.taskErrored(taskId, error); } } diff --git a/packages/cli/src/runners/task-managers/task-manager.ts b/packages/cli/src/runners/task-managers/task-manager.ts index b2ca87bc9c..58d8ade906 100644 --- a/packages/cli/src/runners/task-managers/task-manager.ts +++ b/packages/cli/src/runners/task-managers/task-manager.ts @@ -1,22 +1,23 @@ -import type { - EnvProviderState, - IExecuteFunctions, - Workflow, - IRunExecutionData, - INodeExecutionData, - ITaskDataConnections, - INode, - WorkflowParameters, - INodeParameters, - WorkflowExecuteMode, - IExecuteData, - IDataObject, - IWorkflowExecuteAdditionalData, +import { + type EnvProviderState, + type IExecuteFunctions, + type Workflow, + type IRunExecutionData, + type INodeExecutionData, + type ITaskDataConnections, + type INode, + type WorkflowParameters, + type INodeParameters, + type WorkflowExecuteMode, + type IExecuteData, + type IDataObject, + type IWorkflowExecuteAdditionalData, + type Result, + createResultOk, + createResultError, } from 'n8n-workflow'; import { nanoid } from 'nanoid'; -import { TaskError } from '@/runners/errors'; - import { RPC_ALLOW_LIST, type TaskResultData, @@ -125,7 +126,7 @@ export class TaskManager { tasks: Map = new Map(); - async startTask( + async startTask( additionalData: IWorkflowExecuteAdditionalData, taskType: string, settings: unknown, @@ -145,7 +146,7 @@ export class TaskManager { defaultReturnRunIndex = -1, selfData: IDataObject = {}, contextNodeName: string = activeNodeName, - ): Promise { + ): Promise> { const data: TaskData = { workflow, runExecutionData, @@ -221,14 +222,10 @@ export class TaskManager { runExecutionData.resultData.metadata[k] = v; }); } - return resultData.result as T; - } catch (e) { - if (typeof e === 'string') { - throw new TaskError(e, { - level: 'error', - }); - } - throw e; + + return createResultOk(resultData.result as TData); + } catch (e: unknown) { + return createResultError(e as TError); } finally { this.tasks.delete(taskId); } diff --git a/packages/core/src/Agent/index.ts b/packages/core/src/Agent/index.ts index 1d4e916d33..ed842d99ee 100644 --- a/packages/core/src/Agent/index.ts +++ b/packages/core/src/Agent/index.ts @@ -10,6 +10,7 @@ import type { INodeParameters, IExecuteData, IDataObject, + Result, } from 'n8n-workflow'; import { createEnvProviderState } from 'n8n-workflow'; @@ -29,13 +30,13 @@ export const createAgentStartJob = ( selfData?: IDataObject, contextNodeName?: string, ): IExecuteFunctions['startJob'] => { - return async function startJob( + return async function startJob( this: IExecuteFunctions, jobType: string, settings: unknown, itemIndex: number, - ): Promise { - return await additionalData.startAgentJob( + ): Promise> { + return await additionalData.startAgentJob( additionalData, jobType, settings, diff --git a/packages/nodes-base/nodes/Code/JsTaskRunnerSandbox.ts b/packages/nodes-base/nodes/Code/JsTaskRunnerSandbox.ts index fc0b197f00..37d504be5b 100644 --- a/packages/nodes-base/nodes/Code/JsTaskRunnerSandbox.ts +++ b/packages/nodes-base/nodes/Code/JsTaskRunnerSandbox.ts @@ -1,16 +1,13 @@ import { - ensureError, + ApplicationError, type CodeExecutionMode, type IExecuteFunctions, type INodeExecutionData, type WorkflowExecuteMode, } from 'n8n-workflow'; -import { ExecutionError } from './ExecutionError'; -import { - mapItemsNotDefinedErrorIfNeededForRunForAll, - validateNoDisallowedMethodsInRunForEach, -} from './JsCodeValidator'; +import { isWrappableError, WrappedExecutionError } from './errors/WrappedExecutionError'; +import { validateNoDisallowedMethodsInRunForEach } from './JsCodeValidator'; /** * JS Code execution sandbox that executes the JS code using task runner. @@ -23,70 +20,54 @@ export class JsTaskRunnerSandbox { private readonly executeFunctions: IExecuteFunctions, ) {} - async runCode(): Promise { - const itemIndex = 0; - - try { - const executionResult = (await this.executeFunctions.startJob( - 'javascript', - { - code: this.jsCode, - nodeMode: this.nodeMode, - workflowMode: this.workflowMode, - }, - itemIndex, - )) as T; - return executionResult; - } catch (e) { - const error = ensureError(e); - throw new ExecutionError(error); - } - } - async runCodeAllItems(): Promise { const itemIndex = 0; - return await this.executeFunctions - .startJob( - 'javascript', - { - code: this.jsCode, - nodeMode: this.nodeMode, - workflowMode: this.workflowMode, - continueOnFail: this.executeFunctions.continueOnFail(), - }, - itemIndex, - ) - .catch((e) => { - const error = ensureError(e); - // anticipate user expecting `items` to pre-exist as in Function Item node - mapItemsNotDefinedErrorIfNeededForRunForAll(this.jsCode, error); + const executionResult = await this.executeFunctions.startJob( + 'javascript', + { + code: this.jsCode, + nodeMode: this.nodeMode, + workflowMode: this.workflowMode, + continueOnFail: this.executeFunctions.continueOnFail(), + }, + itemIndex, + ); - throw new ExecutionError(error); - }); + return executionResult.ok + ? executionResult.result + : this.throwExecutionError(executionResult.error); } async runCodeForEachItem(): Promise { validateNoDisallowedMethodsInRunForEach(this.jsCode, 0); const itemIndex = 0; - return await this.executeFunctions - .startJob( - 'javascript', - { - code: this.jsCode, - nodeMode: this.nodeMode, - workflowMode: this.workflowMode, - continueOnFail: this.executeFunctions.continueOnFail(), - }, - itemIndex, - ) - .catch((e) => { - const error = ensureError(e); - // anticipate user expecting `items` to pre-exist as in Function Item node - mapItemsNotDefinedErrorIfNeededForRunForAll(this.jsCode, error); + const executionResult = await this.executeFunctions.startJob( + 'javascript', + { + code: this.jsCode, + nodeMode: this.nodeMode, + workflowMode: this.workflowMode, + continueOnFail: this.executeFunctions.continueOnFail(), + }, + itemIndex, + ); - throw new ExecutionError(error); - }); + return executionResult.ok + ? executionResult.result + : this.throwExecutionError(executionResult.error); + } + + private throwExecutionError(error: unknown): never { + // The error coming from task runner is not an instance of error, + // so we need to wrap it in an error instance. + if (isWrappableError(error)) { + throw new WrappedExecutionError(error); + } + + throw new ApplicationError('Unknown error', { + cause: error, + }); } } diff --git a/packages/nodes-base/nodes/Code/errors/WrappedExecutionError.ts b/packages/nodes-base/nodes/Code/errors/WrappedExecutionError.ts new file mode 100644 index 0000000000..8f6fa40719 --- /dev/null +++ b/packages/nodes-base/nodes/Code/errors/WrappedExecutionError.ts @@ -0,0 +1,35 @@ +import { ApplicationError } from 'n8n-workflow'; + +export type WrappableError = Record; + +/** + * Errors received from the task runner are not instances of Error. + * This class wraps them in an Error instance and makes all their + * properties available. + */ +export class WrappedExecutionError extends ApplicationError { + [key: string]: unknown; + + constructor(error: WrappableError) { + const message = typeof error.message === 'string' ? error.message : 'Unknown error'; + super(message, { + cause: error, + }); + + this.copyErrorProperties(error); + } + + private copyErrorProperties(error: WrappableError) { + for (const key of Object.getOwnPropertyNames(error)) { + if (key === 'message' || key === 'stack') { + continue; + } + + this[key] = error[key]; + } + } +} + +export function isWrappableError(error: unknown): error is WrappableError { + return typeof error === 'object' && error !== null; +} diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 20710c79b0..41a272e47a 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -21,6 +21,7 @@ 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 { ExecutionStatus } from './ExecutionStatus'; +import type { Result } from './result'; import type { Workflow } from './Workflow'; import type { EnvProviderState } from './WorkflowDataProxyEnvProvider'; import type { WorkflowHooks } from './WorkflowHooks'; @@ -997,7 +998,11 @@ export type IExecuteFunctions = ExecuteFunctions.GetNodeParameterFn & getParentCallbackManager(): CallbackManager | undefined; - startJob(jobType: string, settings: unknown, itemIndex: number): Promise; + startJob( + jobType: string, + settings: unknown, + itemIndex: number, + ): Promise>; }; export interface IExecuteSingleFunctions extends BaseExecutionFunctions { @@ -2285,7 +2290,7 @@ export interface IWorkflowExecuteAdditionalData { secretsHelpers: SecretsHelpersBase; logAiEvent: (eventName: AiEvent, payload: AiEventPayload) => void; parentCallbackManager?: CallbackManager; - startAgentJob( + startAgentJob( additionalData: IWorkflowExecuteAdditionalData, jobType: string, settings: unknown, @@ -2305,7 +2310,7 @@ export interface IWorkflowExecuteAdditionalData { defaultReturnRunIndex?: number, selfData?: IDataObject, contextNodeName?: string, - ): Promise; + ): Promise>; } export type WorkflowExecuteMode = @@ -2752,8 +2757,6 @@ export type BannerName = export type Functionality = 'regular' | 'configuration-node' | 'pairedItem'; -export type Result = { ok: true; result: T } | { ok: false; error: E }; - export type CallbackManager = CallbackManagerLC; export type IPersonalizationSurveyAnswersV4 = { diff --git a/packages/workflow/src/NodeParameters/FilterParameter.ts b/packages/workflow/src/NodeParameters/FilterParameter.ts index b2df097b09..88bc80f3ec 100644 --- a/packages/workflow/src/NodeParameters/FilterParameter.ts +++ b/packages/workflow/src/NodeParameters/FilterParameter.ts @@ -7,10 +7,10 @@ import type { FilterOptionsValue, FilterValue, INodeProperties, - Result, ValidationResult, } from '../Interfaces'; import * as LoggerProxy from '../LoggerProxy'; +import type { Result } from '../result'; import { validateFieldType } from '../TypeValidation'; type FilterConditionMetadata = { diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index be5a7ca98e..8c304664f1 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -22,6 +22,7 @@ export * from './WorkflowDataProxyEnvProvider'; export * from './WorkflowHooks'; export * from './VersionedNodeType'; export * from './TypeValidation'; +export * from './result'; export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers }; export { isObjectEmpty, diff --git a/packages/workflow/src/result.ts b/packages/workflow/src/result.ts new file mode 100644 index 0000000000..3ee3c18fb7 --- /dev/null +++ b/packages/workflow/src/result.ts @@ -0,0 +1,13 @@ +export type ResultOk = { ok: true; result: T }; +export type ResultError = { ok: false; error: E }; +export type Result = ResultOk | ResultError; + +export const createResultOk = (data: T): ResultOk => ({ + ok: true, + result: data, +}); + +export const createResultError = (error: E): ResultError => ({ + ok: false, + error, +});