n8n/packages/nodes-base/nodes/Code/Sandbox.ts
Jan Oberhauser 655efeaf66
feat(core): Add optional Error-Output (#7460)
Add an additional optional error output to which all items get sent that
could not be processed.
![Screenshot from 2023-10-18
17-29-15](https://github.com/n8n-io/n8n/assets/6249596/e9732807-ab2b-4662-a5f6-bdff24f7ad55)

Github issue / Community forum post (link here to close automatically):
https://community.n8n.io/t/error-connector-for-nodes/3094

https://community.n8n.io/t/error-handling-at-node-level-detect-node-execution-status/26791

---------

Co-authored-by: OlegIvaniv <me@olegivaniv.com>
2023-10-30 18:42:47 +01:00

169 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { EventEmitter } from 'events';
import type { IExecuteFunctions, INodeExecutionData, IWorkflowDataProxyData } from 'n8n-workflow';
import { ValidationError } from './ValidationError';
import { isObject } from './utils';
interface SandboxTextKeys {
object: {
singular: string;
plural: string;
};
}
export interface SandboxContext extends IWorkflowDataProxyData {
$getNodeParameter: IExecuteFunctions['getNodeParameter'];
$getWorkflowStaticData: IExecuteFunctions['getWorkflowStaticData'];
helpers: IExecuteFunctions['helpers'];
}
export const REQUIRED_N8N_ITEM_KEYS = new Set(['json', 'binary', 'pairedItem', 'error']);
export function getSandboxContext(this: IExecuteFunctions, index: number): SandboxContext {
return {
// from NodeExecuteFunctions
$getNodeParameter: this.getNodeParameter,
$getWorkflowStaticData: this.getWorkflowStaticData,
helpers: this.helpers,
// to bring in all $-prefixed vars and methods from WorkflowDataProxy
// $node, $items(), $parameter, $json, $env, etc.
...this.getWorkflowDataProxy(index),
};
}
export abstract class Sandbox extends EventEmitter {
constructor(
private textKeys: SandboxTextKeys,
protected itemIndex: number | undefined,
protected helpers: IExecuteFunctions['helpers'],
) {
super();
}
abstract runCodeAllItems(): Promise<INodeExecutionData[]>;
abstract runCodeEachItem(): Promise<INodeExecutionData | undefined>;
validateRunCodeEachItem(executionResult: INodeExecutionData | undefined): INodeExecutionData {
if (typeof executionResult !== 'object') {
throw new ValidationError({
message: `Code doesn't return ${this.getTextKey('object', { includeArticle: true })}`,
description: `Please return ${this.getTextKey('object', {
includeArticle: true,
})} representing the output item. ('${executionResult}' was returned instead.)`,
itemIndex: this.itemIndex,
});
}
if (Array.isArray(executionResult)) {
const firstSentence =
executionResult.length > 0
? `An array of ${typeof executionResult[0]}s was returned.`
: 'An empty array was returned.';
throw new ValidationError({
message: `Code doesn't return a single ${this.getTextKey('object')}`,
description: `${firstSentence} If you need to output multiple items, please use the 'Run Once for All Items' mode instead.`,
itemIndex: this.itemIndex,
});
}
const [returnData] = this.helpers.normalizeItems([executionResult]);
this.validateItem(returnData);
// If at least one top-level key is a supported item key (`json`, `binary`, etc.),
// and another top-level key is unrecognized, then the user mis-added a property
// directly on the item, when they intended to add it on the `json` property
this.validateTopLevelKeys(returnData);
return returnData;
}
validateRunCodeAllItems(
executionResult: INodeExecutionData | INodeExecutionData[] | undefined,
itemIndex?: number,
): INodeExecutionData[] {
if (typeof executionResult !== 'object') {
throw new ValidationError({
message: "Code doesn't return items properly",
description: `Please return an array of ${this.getTextKey('object', {
plural: true,
})}, one for each item you would like to output.`,
itemIndex,
});
}
if (Array.isArray(executionResult)) {
/**
* If at least one top-level key is an n8n item key (`json`, `binary`, etc.),
* then require all item keys to be an n8n item key.
*
* If no top-level key is an n8n key, then skip this check, allowing non-n8n
* item keys to be wrapped in `json` when normalizing items below.
*/
const mustHaveTopLevelN8nKey = executionResult.some((item) =>
Object.keys(item).find((key) => REQUIRED_N8N_ITEM_KEYS.has(key)),
);
if (mustHaveTopLevelN8nKey) {
for (const item of executionResult) {
this.validateTopLevelKeys(item);
}
}
}
const returnData = this.helpers.normalizeItems(executionResult);
returnData.forEach((item) => this.validateItem(item));
return returnData;
}
private getTextKey(
key: keyof SandboxTextKeys,
options?: { includeArticle?: boolean; plural?: boolean },
) {
const response = this.textKeys[key][options?.plural ? 'plural' : 'singular'];
if (!options?.includeArticle) {
return response;
}
if (['a', 'e', 'i', 'o', 'u'].some((value) => response.startsWith(value))) {
return `an ${response}`;
}
return `a ${response}`;
}
private validateItem({ json, binary }: INodeExecutionData) {
if (json === undefined || !isObject(json)) {
throw new ValidationError({
message: `A 'json' property isn't ${this.getTextKey('object', { includeArticle: true })}`,
description: `In the returned data, every key named 'json' must point to ${this.getTextKey(
'object',
{ includeArticle: true },
)}.`,
itemIndex: this.itemIndex,
});
}
if (binary !== undefined && !isObject(binary)) {
throw new ValidationError({
message: `A 'binary' property isn't ${this.getTextKey('object', { includeArticle: true })}`,
description: `In the returned data, every key named 'binary must point to ${this.getTextKey(
'object',
{ includeArticle: true },
)}.`,
itemIndex: this.itemIndex,
});
}
}
private validateTopLevelKeys(item: INodeExecutionData) {
Object.keys(item).forEach((key) => {
if (REQUIRED_N8N_ITEM_KEYS.has(key)) return;
throw new ValidationError({
message: `Unknown top-level item key: ${key}`,
description: 'Access the properties of an item under `.json`, e.g. `item.json`',
itemIndex: this.itemIndex,
});
});
}
}