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 type { Task } from '@/task-runner';
|
||||||
|
|
||||||
import { newAllCodeTaskData, newTaskWithSettings, withPairedItem, wrapIntoJson } from './test-data';
|
import { newAllCodeTaskData, newTaskWithSettings, withPairedItem, wrapIntoJson } from './test-data';
|
||||||
|
import { ExecutionError } from '../errors/execution-error';
|
||||||
|
|
||||||
jest.mock('ws');
|
jest.mock('ws');
|
||||||
|
|
||||||
|
@ -292,7 +293,7 @@ describe('JsTaskRunner', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(outcome).toEqual({
|
expect(outcome).toEqual({
|
||||||
result: [wrapIntoJson({ error: 'Error message' })],
|
result: [wrapIntoJson({ error: 'Error message [line 1]' })],
|
||||||
customData: undefined,
|
customData: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -406,8 +407,8 @@ describe('JsTaskRunner', () => {
|
||||||
|
|
||||||
expect(outcome).toEqual({
|
expect(outcome).toEqual({
|
||||||
result: [
|
result: [
|
||||||
withPairedItem(0, wrapIntoJson({ error: 'Error message' })),
|
withPairedItem(0, wrapIntoJson({ error: 'Error message [line 1]' })),
|
||||||
withPairedItem(1, wrapIntoJson({ error: 'Error message' })),
|
withPairedItem(1, wrapIntoJson({ error: 'Error message [line 1]' })),
|
||||||
],
|
],
|
||||||
customData: undefined,
|
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;
|
description: string | null = null;
|
||||||
|
|
||||||
itemIndex: number | undefined = undefined;
|
itemIndex: number | undefined = undefined;
|
||||||
|
@ -11,7 +14,7 @@ export class ExecutionError extends ApplicationError {
|
||||||
|
|
||||||
lineNumber: number | undefined = undefined;
|
lineNumber: number | undefined = undefined;
|
||||||
|
|
||||||
constructor(error: Error & { stack?: string }, itemIndex?: number) {
|
constructor(error: ErrorLike, itemIndex?: number) {
|
||||||
super(error.message);
|
super(error.message);
|
||||||
this.itemIndex = itemIndex;
|
this.itemIndex = itemIndex;
|
||||||
|
|
||||||
|
@ -32,10 +35,11 @@ export class ExecutionError extends ApplicationError {
|
||||||
|
|
||||||
if (stackRows.length === 0) {
|
if (stackRows.length === 0) {
|
||||||
this.message = 'Unknown error';
|
this.message = 'Unknown error';
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageRow = stackRows.find((line) => line.includes('Error:'));
|
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);
|
const lineNumberDisplay = this.toLineNumberDisplay(lineNumberRow);
|
||||||
|
|
||||||
if (!messageRow) {
|
if (!messageRow) {
|
||||||
|
@ -56,16 +60,22 @@ export class ExecutionError extends ApplicationError {
|
||||||
}
|
}
|
||||||
|
|
||||||
private toLineNumberDisplay(lineNumberRow?: string) {
|
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;
|
if (!errorLineNumberMatch?.groups?.lineNumber) return null;
|
||||||
|
|
||||||
const lineNumber = errorLineNumberMatch.groups.lineNumber;
|
const lineNumber = errorLineNumberMatch.groups.lineNumber;
|
||||||
|
if (!lineNumber) return '';
|
||||||
|
|
||||||
this.lineNumber = Number(lineNumber);
|
this.lineNumber = Number(lineNumber);
|
||||||
|
|
||||||
if (!lineNumber) return '';
|
|
||||||
|
|
||||||
return this.itemIndex === undefined
|
return this.itemIndex === undefined
|
||||||
? `[line ${lineNumber}]`
|
? `[line ${lineNumber}]`
|
||||||
: `[line ${lineNumber}, for item ${this.itemIndex}]`;
|
: `[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 = '';
|
description = '';
|
||||||
|
|
||||||
itemIndex: number | undefined = undefined;
|
itemIndex: number | undefined = undefined;
|
||||||
|
|
|
@ -25,6 +25,8 @@ import { runInNewContext, type Context } from 'node:vm';
|
||||||
import type { TaskResultData } from '@/runner-types';
|
import type { TaskResultData } from '@/runner-types';
|
||||||
import { type Task, TaskRunner } from '@/task-runner';
|
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 type { RequireResolver } from './require-resolver';
|
||||||
import { createRequireResolver } from './require-resolver';
|
import { createRequireResolver } from './require-resolver';
|
||||||
import { validateRunForAllItemsOutput, validateRunForEachItemOutput } from './result-validation';
|
import { validateRunForAllItemsOutput, validateRunForEachItemOutput } from './result-validation';
|
||||||
|
@ -186,7 +188,7 @@ export class JsTaskRunner extends TaskRunner {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = (await runInNewContext(
|
const result = (await runInNewContext(
|
||||||
`module.exports = async function() {${settings.code}\n}()`,
|
`module.exports = async function VmCodeWrapper() {${settings.code}\n}()`,
|
||||||
context,
|
context,
|
||||||
)) as TaskResultData['result'];
|
)) as TaskResultData['result'];
|
||||||
|
|
||||||
|
@ -195,12 +197,14 @@ export class JsTaskRunner extends TaskRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
return validateRunForAllItemsOutput(result);
|
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) {
|
if (settings.continueOnFail) {
|
||||||
return [{ json: { error: this.getErrorMessageFromVmError(error) } }];
|
return [{ json: { error: error.message } }];
|
||||||
}
|
}
|
||||||
|
|
||||||
(error as Record<string, unknown>).node = allData.node;
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,7 +237,7 @@ export class JsTaskRunner extends TaskRunner {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result = (await runInNewContext(
|
let result = (await runInNewContext(
|
||||||
`module.exports = async function() {${settings.code}\n}()`,
|
`module.exports = async function VmCodeWrapper() {${settings.code}\n}()`,
|
||||||
context,
|
context,
|
||||||
)) as INodeExecutionData | undefined;
|
)) 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) {
|
if (!settings.continueOnFail) {
|
||||||
(error as Record<string, unknown>).node = allData.node;
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
returnData.push({
|
returnData.push({
|
||||||
json: { error: this.getErrorMessageFromVmError(error) },
|
json: { error: error.message },
|
||||||
pairedItem: {
|
pairedItem: {
|
||||||
item: index,
|
item: index,
|
||||||
},
|
},
|
||||||
|
@ -304,11 +310,15 @@ export class JsTaskRunner extends TaskRunner {
|
||||||
).getDataProxy();
|
).getDataProxy();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getErrorMessageFromVmError(error: unknown): string {
|
private toExecutionErrorIfNeeded(error: unknown): Error {
|
||||||
if (typeof error === 'object' && !!error && 'message' in error) {
|
if (error instanceof Error) {
|
||||||
return error.message as string;
|
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 { nanoid } from 'nanoid';
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import { type MessageEvent, WebSocket } from 'ws';
|
import { type MessageEvent, WebSocket } from 'ws';
|
||||||
|
@ -256,8 +256,7 @@ export abstract class TaskRunner {
|
||||||
try {
|
try {
|
||||||
const data = await this.executeTask(task);
|
const data = await this.executeTask(task);
|
||||||
this.taskDone(taskId, data);
|
this.taskDone(taskId, data);
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
const error = ensureError(e);
|
|
||||||
this.taskErrored(taskId, error);
|
this.taskErrored(taskId, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
import type {
|
import {
|
||||||
EnvProviderState,
|
type EnvProviderState,
|
||||||
IExecuteFunctions,
|
type IExecuteFunctions,
|
||||||
Workflow,
|
type Workflow,
|
||||||
IRunExecutionData,
|
type IRunExecutionData,
|
||||||
INodeExecutionData,
|
type INodeExecutionData,
|
||||||
ITaskDataConnections,
|
type ITaskDataConnections,
|
||||||
INode,
|
type INode,
|
||||||
WorkflowParameters,
|
type WorkflowParameters,
|
||||||
INodeParameters,
|
type INodeParameters,
|
||||||
WorkflowExecuteMode,
|
type WorkflowExecuteMode,
|
||||||
IExecuteData,
|
type IExecuteData,
|
||||||
IDataObject,
|
type IDataObject,
|
||||||
IWorkflowExecuteAdditionalData,
|
type IWorkflowExecuteAdditionalData,
|
||||||
|
type Result,
|
||||||
|
createResultOk,
|
||||||
|
createResultError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
import { TaskError } from '@/runners/errors';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RPC_ALLOW_LIST,
|
RPC_ALLOW_LIST,
|
||||||
type TaskResultData,
|
type TaskResultData,
|
||||||
|
@ -125,7 +126,7 @@ export class TaskManager {
|
||||||
|
|
||||||
tasks: Map<string, Task> = new Map();
|
tasks: Map<string, Task> = new Map();
|
||||||
|
|
||||||
async startTask<T>(
|
async startTask<TData, TError>(
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
taskType: string,
|
taskType: string,
|
||||||
settings: unknown,
|
settings: unknown,
|
||||||
|
@ -145,7 +146,7 @@ export class TaskManager {
|
||||||
defaultReturnRunIndex = -1,
|
defaultReturnRunIndex = -1,
|
||||||
selfData: IDataObject = {},
|
selfData: IDataObject = {},
|
||||||
contextNodeName: string = activeNodeName,
|
contextNodeName: string = activeNodeName,
|
||||||
): Promise<T> {
|
): Promise<Result<TData, TError>> {
|
||||||
const data: TaskData = {
|
const data: TaskData = {
|
||||||
workflow,
|
workflow,
|
||||||
runExecutionData,
|
runExecutionData,
|
||||||
|
@ -221,14 +222,10 @@ export class TaskManager {
|
||||||
runExecutionData.resultData.metadata[k] = v;
|
runExecutionData.resultData.metadata[k] = v;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return resultData.result as T;
|
|
||||||
} catch (e) {
|
return createResultOk(resultData.result as TData);
|
||||||
if (typeof e === 'string') {
|
} catch (e: unknown) {
|
||||||
throw new TaskError(e, {
|
return createResultError(e as TError);
|
||||||
level: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
} finally {
|
} finally {
|
||||||
this.tasks.delete(taskId);
|
this.tasks.delete(taskId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
||||||
INodeParameters,
|
INodeParameters,
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
|
Result,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { createEnvProviderState } from 'n8n-workflow';
|
import { createEnvProviderState } from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -29,13 +30,13 @@ export const createAgentStartJob = (
|
||||||
selfData?: IDataObject,
|
selfData?: IDataObject,
|
||||||
contextNodeName?: string,
|
contextNodeName?: string,
|
||||||
): IExecuteFunctions['startJob'] => {
|
): IExecuteFunctions['startJob'] => {
|
||||||
return async function startJob<T = unknown>(
|
return async function startJob<T = unknown, E = unknown>(
|
||||||
this: IExecuteFunctions,
|
this: IExecuteFunctions,
|
||||||
jobType: string,
|
jobType: string,
|
||||||
settings: unknown,
|
settings: unknown,
|
||||||
itemIndex: number,
|
itemIndex: number,
|
||||||
): Promise<T> {
|
): Promise<Result<T, E>> {
|
||||||
return await additionalData.startAgentJob<T>(
|
return await additionalData.startAgentJob<T, E>(
|
||||||
additionalData,
|
additionalData,
|
||||||
jobType,
|
jobType,
|
||||||
settings,
|
settings,
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
import {
|
import {
|
||||||
ensureError,
|
ApplicationError,
|
||||||
type CodeExecutionMode,
|
type CodeExecutionMode,
|
||||||
type IExecuteFunctions,
|
type IExecuteFunctions,
|
||||||
type INodeExecutionData,
|
type INodeExecutionData,
|
||||||
type WorkflowExecuteMode,
|
type WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { ExecutionError } from './ExecutionError';
|
import { isWrappableError, WrappedExecutionError } from './errors/WrappedExecutionError';
|
||||||
import {
|
import { validateNoDisallowedMethodsInRunForEach } from './JsCodeValidator';
|
||||||
mapItemsNotDefinedErrorIfNeededForRunForAll,
|
|
||||||
validateNoDisallowedMethodsInRunForEach,
|
|
||||||
} from './JsCodeValidator';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JS Code execution sandbox that executes the JS code using task runner.
|
* JS Code execution sandbox that executes the JS code using task runner.
|
||||||
|
@ -23,70 +20,54 @@ export class JsTaskRunnerSandbox {
|
||||||
private readonly executeFunctions: IExecuteFunctions,
|
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[]> {
|
async runCodeAllItems(): Promise<INodeExecutionData[]> {
|
||||||
const itemIndex = 0;
|
const itemIndex = 0;
|
||||||
|
|
||||||
return await this.executeFunctions
|
const executionResult = await this.executeFunctions.startJob<INodeExecutionData[]>(
|
||||||
.startJob<INodeExecutionData[]>(
|
'javascript',
|
||||||
'javascript',
|
{
|
||||||
{
|
code: this.jsCode,
|
||||||
code: this.jsCode,
|
nodeMode: this.nodeMode,
|
||||||
nodeMode: this.nodeMode,
|
workflowMode: this.workflowMode,
|
||||||
workflowMode: this.workflowMode,
|
continueOnFail: this.executeFunctions.continueOnFail(),
|
||||||
continueOnFail: this.executeFunctions.continueOnFail(),
|
},
|
||||||
},
|
itemIndex,
|
||||||
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[]> {
|
async runCodeForEachItem(): Promise<INodeExecutionData[]> {
|
||||||
validateNoDisallowedMethodsInRunForEach(this.jsCode, 0);
|
validateNoDisallowedMethodsInRunForEach(this.jsCode, 0);
|
||||||
const itemIndex = 0;
|
const itemIndex = 0;
|
||||||
|
|
||||||
return await this.executeFunctions
|
const executionResult = await this.executeFunctions.startJob<INodeExecutionData[]>(
|
||||||
.startJob<INodeExecutionData[]>(
|
'javascript',
|
||||||
'javascript',
|
{
|
||||||
{
|
code: this.jsCode,
|
||||||
code: this.jsCode,
|
nodeMode: this.nodeMode,
|
||||||
nodeMode: this.nodeMode,
|
workflowMode: this.workflowMode,
|
||||||
workflowMode: this.workflowMode,
|
continueOnFail: this.executeFunctions.continueOnFail(),
|
||||||
continueOnFail: this.executeFunctions.continueOnFail(),
|
},
|
||||||
},
|
itemIndex,
|
||||||
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 { WorkflowActivationError } from './errors/workflow-activation.error';
|
||||||
import type { WorkflowOperationError } from './errors/workflow-operation.error';
|
import type { WorkflowOperationError } from './errors/workflow-operation.error';
|
||||||
import type { ExecutionStatus } from './ExecutionStatus';
|
import type { ExecutionStatus } from './ExecutionStatus';
|
||||||
|
import type { Result } from './result';
|
||||||
import type { Workflow } from './Workflow';
|
import type { Workflow } from './Workflow';
|
||||||
import type { EnvProviderState } from './WorkflowDataProxyEnvProvider';
|
import type { EnvProviderState } from './WorkflowDataProxyEnvProvider';
|
||||||
import type { WorkflowHooks } from './WorkflowHooks';
|
import type { WorkflowHooks } from './WorkflowHooks';
|
||||||
|
@ -997,7 +998,11 @@ export type IExecuteFunctions = ExecuteFunctions.GetNodeParameterFn &
|
||||||
|
|
||||||
getParentCallbackManager(): CallbackManager | undefined;
|
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 {
|
export interface IExecuteSingleFunctions extends BaseExecutionFunctions {
|
||||||
|
@ -2285,7 +2290,7 @@ export interface IWorkflowExecuteAdditionalData {
|
||||||
secretsHelpers: SecretsHelpersBase;
|
secretsHelpers: SecretsHelpersBase;
|
||||||
logAiEvent: (eventName: AiEvent, payload: AiEventPayload) => void;
|
logAiEvent: (eventName: AiEvent, payload: AiEventPayload) => void;
|
||||||
parentCallbackManager?: CallbackManager;
|
parentCallbackManager?: CallbackManager;
|
||||||
startAgentJob<T>(
|
startAgentJob<T, E = unknown>(
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
jobType: string,
|
jobType: string,
|
||||||
settings: unknown,
|
settings: unknown,
|
||||||
|
@ -2305,7 +2310,7 @@ export interface IWorkflowExecuteAdditionalData {
|
||||||
defaultReturnRunIndex?: number,
|
defaultReturnRunIndex?: number,
|
||||||
selfData?: IDataObject,
|
selfData?: IDataObject,
|
||||||
contextNodeName?: string,
|
contextNodeName?: string,
|
||||||
): Promise<T>;
|
): Promise<Result<T, E>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkflowExecuteMode =
|
export type WorkflowExecuteMode =
|
||||||
|
@ -2752,8 +2757,6 @@ export type BannerName =
|
||||||
|
|
||||||
export type Functionality = 'regular' | 'configuration-node' | 'pairedItem';
|
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 CallbackManager = CallbackManagerLC;
|
||||||
|
|
||||||
export type IPersonalizationSurveyAnswersV4 = {
|
export type IPersonalizationSurveyAnswersV4 = {
|
||||||
|
|
|
@ -7,10 +7,10 @@ import type {
|
||||||
FilterOptionsValue,
|
FilterOptionsValue,
|
||||||
FilterValue,
|
FilterValue,
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
Result,
|
|
||||||
ValidationResult,
|
ValidationResult,
|
||||||
} from '../Interfaces';
|
} from '../Interfaces';
|
||||||
import * as LoggerProxy from '../LoggerProxy';
|
import * as LoggerProxy from '../LoggerProxy';
|
||||||
|
import type { Result } from '../result';
|
||||||
import { validateFieldType } from '../TypeValidation';
|
import { validateFieldType } from '../TypeValidation';
|
||||||
|
|
||||||
type FilterConditionMetadata = {
|
type FilterConditionMetadata = {
|
||||||
|
|
|
@ -22,6 +22,7 @@ export * from './WorkflowDataProxyEnvProvider';
|
||||||
export * from './WorkflowHooks';
|
export * from './WorkflowHooks';
|
||||||
export * from './VersionedNodeType';
|
export * from './VersionedNodeType';
|
||||||
export * from './TypeValidation';
|
export * from './TypeValidation';
|
||||||
|
export * from './result';
|
||||||
export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers };
|
export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers };
|
||||||
export {
|
export {
|
||||||
isObjectEmpty,
|
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