mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 04:47:29 -08:00
fix: Make sure errors are transferred correctly from js task runner (no-changelog) (#11214)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
This commit is contained in:
parent
4e78c46a74
commit
1078fa662a
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { ExecutionError } from '../execution-error';
|
||||
|
||||
describe('ExecutionError', () => {
|
||||
const defaultStack = `TypeError: a.unknown is not a function
|
||||
at VmCodeWrapper (evalmachine.<anonymous>:2:3)
|
||||
at evalmachine.<anonymous>: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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
}
|
|
@ -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:(?<lineNumber>\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\\.<anonymous>:(?<lineNumber>\\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}]`;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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<string, unknown>).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<string, unknown>).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) });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<string, Task> = new Map();
|
||||
|
||||
async startTask<T>(
|
||||
async startTask<TData, TError>(
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
taskType: string,
|
||||
settings: unknown,
|
||||
|
@ -145,7 +146,7 @@ export class TaskManager {
|
|||
defaultReturnRunIndex = -1,
|
||||
selfData: IDataObject = {},
|
||||
contextNodeName: string = activeNodeName,
|
||||
): Promise<T> {
|
||||
): Promise<Result<TData, TError>> {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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<T = unknown>(
|
||||
return async function startJob<T = unknown, E = unknown>(
|
||||
this: IExecuteFunctions,
|
||||
jobType: string,
|
||||
settings: unknown,
|
||||
itemIndex: number,
|
||||
): Promise<T> {
|
||||
return await additionalData.startAgentJob<T>(
|
||||
): Promise<Result<T, E>> {
|
||||
return await additionalData.startAgentJob<T, E>(
|
||||
additionalData,
|
||||
jobType,
|
||||
settings,
|
||||
|
|
|
@ -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,31 +20,10 @@ export class JsTaskRunnerSandbox {
|
|||
private readonly executeFunctions: IExecuteFunctions,
|
||||
) {}
|
||||
|
||||
async runCode<T = unknown>(): Promise<T> {
|
||||
const itemIndex = 0;
|
||||
|
||||
try {
|
||||
const executionResult = (await this.executeFunctions.startJob<T>(
|
||||
'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<INodeExecutionData[]> {
|
||||
const itemIndex = 0;
|
||||
|
||||
return await this.executeFunctions
|
||||
.startJob<INodeExecutionData[]>(
|
||||
const executionResult = await this.executeFunctions.startJob<INodeExecutionData[]>(
|
||||
'javascript',
|
||||
{
|
||||
code: this.jsCode,
|
||||
|
@ -56,22 +32,18 @@ export class JsTaskRunnerSandbox {
|
|||
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);
|
||||
);
|
||||
|
||||
throw new ExecutionError(error);
|
||||
});
|
||||
return executionResult.ok
|
||||
? executionResult.result
|
||||
: this.throwExecutionError(executionResult.error);
|
||||
}
|
||||
|
||||
async runCodeForEachItem(): Promise<INodeExecutionData[]> {
|
||||
validateNoDisallowedMethodsInRunForEach(this.jsCode, 0);
|
||||
const itemIndex = 0;
|
||||
|
||||
return await this.executeFunctions
|
||||
.startJob<INodeExecutionData[]>(
|
||||
const executionResult = await this.executeFunctions.startJob<INodeExecutionData[]>(
|
||||
'javascript',
|
||||
{
|
||||
code: this.jsCode,
|
||||
|
@ -80,13 +52,22 @@ export class JsTaskRunnerSandbox {
|
|||
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);
|
||||
);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import { ApplicationError } from 'n8n-workflow';
|
||||
|
||||
export type WrappableError = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
|
@ -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<T = unknown>(jobType: string, settings: unknown, itemIndex: number): Promise<T>;
|
||||
startJob<T = unknown, E = unknown>(
|
||||
jobType: string,
|
||||
settings: unknown,
|
||||
itemIndex: number,
|
||||
): Promise<Result<T, E>>;
|
||||
};
|
||||
|
||||
export interface IExecuteSingleFunctions extends BaseExecutionFunctions {
|
||||
|
@ -2285,7 +2290,7 @@ export interface IWorkflowExecuteAdditionalData {
|
|||
secretsHelpers: SecretsHelpersBase;
|
||||
logAiEvent: (eventName: AiEvent, payload: AiEventPayload) => void;
|
||||
parentCallbackManager?: CallbackManager;
|
||||
startAgentJob<T>(
|
||||
startAgentJob<T, E = unknown>(
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
jobType: string,
|
||||
settings: unknown,
|
||||
|
@ -2305,7 +2310,7 @@ export interface IWorkflowExecuteAdditionalData {
|
|||
defaultReturnRunIndex?: number,
|
||||
selfData?: IDataObject,
|
||||
contextNodeName?: string,
|
||||
): Promise<T>;
|
||||
): Promise<Result<T, E>>;
|
||||
}
|
||||
|
||||
export type WorkflowExecuteMode =
|
||||
|
@ -2752,8 +2757,6 @@ export type BannerName =
|
|||
|
||||
export type Functionality = 'regular' | 'configuration-node' | 'pairedItem';
|
||||
|
||||
export type Result<T, E> = { ok: true; result: T } | { ok: false; error: E };
|
||||
|
||||
export type CallbackManager = CallbackManagerLC;
|
||||
|
||||
export type IPersonalizationSurveyAnswersV4 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
13
packages/workflow/src/result.ts
Normal file
13
packages/workflow/src/result.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export type ResultOk<T> = { ok: true; result: T };
|
||||
export type ResultError<E> = { ok: false; error: E };
|
||||
export type Result<T, E> = ResultOk<T> | ResultError<E>;
|
||||
|
||||
export const createResultOk = <T>(data: T): ResultOk<T> => ({
|
||||
ok: true,
|
||||
result: data,
|
||||
});
|
||||
|
||||
export const createResultError = <E = unknown>(error: E): ResultError<E> => ({
|
||||
ok: false,
|
||||
error,
|
||||
});
|
Loading…
Reference in a new issue