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

This commit is contained in:
Tomi Turtiainen 2024-10-10 21:01:38 +03:00 committed by GitHub
parent 4e78c46a74
commit 1078fa662a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 311 additions and 122 deletions

View file

@ -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);
});
}); });

View file

@ -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,
}),
);
});
});

View file

@ -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';
}

View file

@ -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}]`;

View file

@ -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,
});
}
}

View file

@ -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;

View file

@ -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) });
} }
} }

View file

@ -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);
} }
} }

View file

@ -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);
} }

View file

@ -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,

View file

@ -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,
});
} }
} }

View file

@ -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;
}

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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,

View 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,
});