Files
Automata/packages/workflow/src/NodeErrors.ts
Iván Ovejero 1db4fa2bf8 feat(Code Node): create Code node (#3965)
* Introduce node deprecation (#3930)

 Introduce node deprecation

* 🚧 Scaffold out Code node

* 👕 Fix lint

* 📘 Create types file

* 🚚 Rename theme

* 🔥 Remove unneeded prop

*  Override keybindings

*  Expand lintings

*  Create editor content getter

* 🚚 Ensure all helpers use `$`

*  Add autocompletion

*  Filter out welcome note node

*  Convey error line number

*  Highlight error line

*  Restore logging from node

*  More autocompletions

*  Streamline completions

* ✏️ Update placeholders

*  Update linter to new methods

* 🔥 Remove `$nodeItem` completions

*  Re-update placeholders

* 🎨 Fix formatting

* 📦 Update `package-lock.json`

*  Refresh with multi-line empty string

*  Account for syntax errors

* 🔥 Remove unneeded variant

*  Minor improvements

*  Add more autocompletions

* 🚚 Rename extension

* 🔥 Remove outdated comments

* 🚚 Rename field

*  More autocompletions

*  Fix up error display when empty text

* 🔥 Remove logging

*  More error validation

* 🐛 Fix `pairedItem` to `pairedItem()`

*  Add item to validation info

* 📦 Update `package-lock.json`

*  Leftover fixes

*  Set `insertNewlineAndIndent`

* 📦 Update `package-lock.json`

* 📦 Re-update `package-lock.json`

* 👕 Add lint exception

* 📘 Add type to mixin type

* Clean up comment

*  Refactor completion per new requirements

*  Adjust placeholders

*  Add `json` autocompletions for `$input`

* 🎨 Set border

*  Restore local completion source

*  Implement autocompletion for imports

*  Add `.*` to follow user typing on autocompletion

* 📘 Fix typings in autocompletions

* 👕 Add linting for use of `item()`

* 📦 Update `package-lock.json`

* 🐛 Fix for `$items(nodeName)[0]`

*  Filter down built-in modules list

*  Refactor error handling

*  Linter and validation improvements

*  Apply review feedback

* ♻️ More general refactorings

*  Add dot notation utility

* Customize input handler

*  Support `.json.` completions

*  Adjust placeholder

*  Sort imports

* 🔥 Remove blank rows addition

*  Add more error validation

* 📦 Update `package-lock.json`

*  Make date logging consistent

* 🔧 Adjust linting highlight range

*  Add line numbers to each item mode errors

*  Allow for links in error descriptions

*  More input validation

*  Expand linting to loops

*  Deprecate Function and Function Item nodes

* 🐛 Fix placeholder syntax

* 📘 Narrow down type

* 🚚 Rename using kebab-case

* 🔥 Remove `mapGetters`

* ✏️ Fix casing

*  Adjust import for type

* ✏️ Fix quotes

* 🐛 Fix `activeNode` reference

*  Use constant

* 🔥 Remove logging

* ✏️ Fix typo

*  Add missing `notice`

* ✏️ Add tags

* ✏️ Fix alias

* ✏️ Update copy

* 🔥 Remove wrong linting

* ✏️ Update copy

*  Add validation for `null`

*  Add validation for non-object and non-array

*  Add validation for non-array with json

* ✏️ Intentionally use wrong spelling

*  More validation

* ✏️ More copy updates

* ✏️ Placeholder updates

*  Restore spelling

*  Fix var name

* ✏️ More copy updates

*  Add luxon autocompletions

*  Make scrollable

*  Fix comma from merge conflict resolution

* 📦 Update `package-lock.json`

* 👕 Fix lint detail

* 🎨 Set font family

*  Bring in expressions fix

* ♻️ Address feedback

*  Exclude codemirror packages from render chunks

* 🐛 Fix placeholder not showing on first load

* feat(editor-ui): Replace `lezer` with `esprima` in client linter (#4192)

* 🔥 Remove addition from misresolved conflict

*  Replace `lezer` with `esprima` in client linter

*  Add missing key

* 📦 Update `package-lock.json`

*  Match dependencies

* 📦 Update `package-lock.json`

* 📦 Re-update `package-lock.json`

*  Match whitespace

* 🐛 Fix selection

*  Expand validation

* 🔥 Remove validation

* ✏️ Update copy

* 🚚 Move to constants

*  More `null` validation

*  Support `all()` with index to access item

*  Gloss over n8n syntax error

* 🎨 Re-style diagnostic button

* 🔥 Remove `item` as `itemAlias`

*  Add linting for `item.json` in single item mode

*  Refactor to add label info descriptions

*  More autocompletions

* 👕 Fix lint

*  Simplify typings

* feat(nodes-base): Multiline autocompletion for `code-node-editor` (#4220)

*  Simplify typings

*  Consolidate helpers in utils

*  Multiline autocompletion for standalone vars

* 🔥 Remove unneeded mixins

* ✏️ Update copy

* ✏️ Prep TODOs

*  Multiline completion for `$input.method` + `$input.item`

* 🔥 Remove unused method

* 🔥 Remove another unused method

* 🚚 Move luxon strings to helpers

*  Multiline autocompletion for methods output

*  Refactor to use optional chaining

* 👕 Fix lint

* ✏️ Update TODOs

*  Multiline autocompletion for `json` fields

* 📘 Add typings

*  De-duplicate callback to forEach

* 🐛 Fix autocompletions not working with leading whitespace

* 🌐 Apply i18n

* 👕 Fix lint

* :constructor: Second-period var usage completions

* 👕 Fix lint

* 👕 Add exception

*  Add completion telemetry

* 📘 Add typing

*  Major refactoring to organize

* 🐛 Fix multiline `.all()[index]`

* 🐛 Do not autoclose square brackets prior to `.json`

* 🐛 Fix accessor for multiline `jsonField` completions

*  Add completions for half-assignments

* 🐛 Fix `jsonField` completions for `x.json`

* ✏️ Improve comments

* 🐛 Fix `.json[field]` for multiline matches

*  Cleanup

* 📦 Update `package-lock.json`

* 👕 Fix lint

* 🐛 Rely on original value for custom matcher

*  Create `customMatcherJsonFieldCompletions` to simplify setup

* 🐛 Include selector in `customMatcherJsonField` completions

* ✏️ Make naming consistent

* ✏️ Add docline

*  Finish self-review cleanup

* 🔥 Remove outdated comment

* 📌 Pin luxon to major-minor

* ✏️ Fix typo

* 📦 Update `package-lock.json`

* 📦 Update `package-lock.json`

* 📦 Re-update `package-lock.json`

*  Add `luxon` for Gmail node

* 📦 Update `package-lock.json`

*  Replace Function with Code in suggested nodes

* 🐛 Fix `$prevNode` completions

* ✏️ Update `$execution.mode` copy

*  Separate luxon getters from methods

*  Adjusting linter to tolerate `.binary`

*  Adjust top-level item keys check

*  Anticipate user expecting `item` to pre-exist

*  Add linting for legacy item access

*  Add hint for attempted `items` access

*  Add keybinding for toggling comments

* ✏️ Update copy of `all`, `first`, `last` and `itemMatching`

* 🐛 Make `input.all()` etc act on copies

* 📦 Update `package-lock.json`

* 🐛 Fix guard in `$input.last()`

* ♻️ Address Jan's feedback

* ⬆️ Upgrade `eslint-plugin-n8n-nodes-base`

* 📦 Update `package-lock.json`

* 🔥 Remove unneeded exceptions

*  Restore placeholder logic

*  Add placeholders to client

*  Account for shadow item

* ✏️ More completion info labels

* 👕 Fix lint

* ✏️ Update copy

* ✏️ Update copy

* ✏️ More copy updates

* 📦 Update `package-lock.json`

*  Add more validation

*  Add placheolder on first load

* Replace `Cmd` with `Mod`

* 📦 Update `package-lock.json`
2022-10-13 14:28:02 +02:00

363 lines
9.8 KiB
TypeScript

/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
// eslint-disable-next-line max-classes-per-file
import { parseString } from 'xml2js';
import { IDataObject, INode, IStatusCodeMessages, JsonObject } from './Interfaces';
/**
* Top-level properties where an error message can be found in an API response.
*/
const ERROR_MESSAGE_PROPERTIES = [
'cause',
'error',
'message',
'Message',
'msg',
'messages',
'description',
'reason',
'detail',
'details',
'errors',
'errorMessage',
'errorMessages',
'ErrorMessage',
'error_message',
'_error_message',
'errorDescription',
'error_description',
'error_summary',
'title',
'text',
'field',
'err',
'type',
];
/**
* Top-level properties where an HTTP error code can be found in an API response.
*/
const ERROR_STATUS_PROPERTIES = [
'statusCode',
'status',
'code',
'status_code',
'errorCode',
'error_code',
];
/**
* Properties where a nested object can be found in an API response.
*/
const ERROR_NESTING_PROPERTIES = ['error', 'err', 'response', 'body', 'data'];
export abstract class ExecutionBaseError extends Error {
description: string | null | undefined;
cause: Error | JsonObject;
timestamp: number;
context: IDataObject = {};
lineNumber: number | undefined;
constructor(error: Error | ExecutionBaseError | JsonObject) {
super();
this.name = this.constructor.name;
this.cause = error;
this.timestamp = Date.now();
if (error.message) {
this.message = error.message as string;
}
if (error instanceof ExecutionBaseError) {
this.context = error.context;
}
}
}
/**
* Base class for specific NodeError-types, with functionality for finding
* a value recursively inside an error object.
*/
abstract class NodeError extends ExecutionBaseError {
node: INode;
constructor(node: INode, error: Error | JsonObject) {
super(error);
this.node = node;
}
/**
* Finds property through exploration based on potential keys and traversal keys.
* Depth-first approach.
*
* This method iterates over `potentialKeys` and, if the value at the key is a
* truthy value, the type of the value is checked:
* (1) if a string or number, the value is returned as a string; or
* (2) if an array,
* its string or number elements are collected as a long string,
* its object elements are traversed recursively (restart this function
* with each object as a starting point), or
* (3) if it is an object, it traverses the object and nested ones recursively
* based on the `potentialKeys` and returns a string if found.
*
* If nothing found via `potentialKeys` this method iterates over `traversalKeys` and
* if the value at the key is a traversable object, it restarts with the object as the
* new starting point (recursion).
* If nothing found for any of the `traversalKeys`, exploration continues with remaining
* `traversalKeys`.
*
* Otherwise, if all the paths have been exhausted and no value is eligible, `null` is
* returned.
*
*/
protected findProperty(
error: JsonObject,
potentialKeys: string[],
traversalKeys: string[] = [],
): string | null {
// eslint-disable-next-line no-restricted-syntax
for (const key of potentialKeys) {
const value = error[key];
if (value) {
if (typeof value === 'string') return value;
if (typeof value === 'number') return value.toString();
if (Array.isArray(value)) {
const resolvedErrors: string[] = value
.map((error) => {
if (typeof error === 'string') return error;
if (typeof error === 'number') return error.toString();
if (this.isTraversableObject(error)) {
return this.findProperty(error, potentialKeys);
}
return null;
})
.filter((errorValue): errorValue is string => errorValue !== null);
if (resolvedErrors.length === 0) {
return null;
}
return resolvedErrors.join(' | ');
}
if (this.isTraversableObject(value)) {
const property = this.findProperty(value, potentialKeys);
if (property) {
return property;
}
}
}
}
// eslint-disable-next-line no-restricted-syntax
for (const key of traversalKeys) {
const value = error[key];
if (this.isTraversableObject(value)) {
const property = this.findProperty(value, potentialKeys, traversalKeys);
if (property) {
return property;
}
}
}
return null;
}
/**
* Check if a value is an object with at least one key, i.e. it can be traversed.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected isTraversableObject(value: any): value is JsonObject {
return (
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
value && typeof value === 'object' && !Array.isArray(value) && !!Object.keys(value).length
);
}
/**
* Remove circular references from objects.
*/
protected removeCircularRefs(obj: JsonObject, seen = new Set()) {
seen.add(obj);
Object.entries(obj).forEach(([key, value]) => {
if (this.isTraversableObject(value)) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
seen.has(value)
? (obj[key] = { circularReference: true })
: this.removeCircularRefs(value, seen);
return;
}
if (Array.isArray(value)) {
value.forEach((val, index) => {
if (seen.has(val)) {
value[index] = { circularReference: true };
return;
}
if (this.isTraversableObject(val)) {
this.removeCircularRefs(val, seen);
}
});
}
});
}
}
/**
* Class for instantiating an operational error, e.g. an invalid credentials error.
*/
export class NodeOperationError extends NodeError {
lineNumber: number | undefined;
constructor(
node: INode,
error: Error | string,
options?: { description?: string; runIndex?: number; itemIndex?: number },
) {
if (typeof error === 'string') {
error = new Error(error);
}
super(node, error);
if (options?.description) {
this.description = options.description;
}
if (options?.runIndex !== undefined) {
this.context.runIndex = options.runIndex;
}
if (options?.itemIndex !== undefined) {
this.context.itemIndex = options.itemIndex;
}
}
}
const STATUS_CODE_MESSAGES: IStatusCodeMessages = {
'4XX': 'Your request is invalid or could not be processed by the service',
'400': 'Bad request - please check your parameters',
'401': 'Authorization failed - please check your credentials',
'402': 'Payment required - perhaps check your payment details?',
'403': 'Forbidden - perhaps check your credentials?',
'404': 'The resource you are requesting could not be found',
'405': 'Method not allowed - please check you are using the right HTTP method',
'429': 'The service is receiving too many requests from you! Perhaps take a break?',
'5XX': 'The service failed to process your request',
'500': 'The service was not able to process your request',
'502': 'Bad gateway - the service failed to handle your request',
'503': 'Service unavailable - perhaps try again later?',
'504': 'Gateway timed out - perhaps try again later?',
};
const UNKNOWN_ERROR_MESSAGE = 'UNKNOWN ERROR - check the detailed error for more information';
/**
* Class for instantiating an error in an API response, e.g. a 404 Not Found response,
* with an HTTP error code, an error message and a description.
*/
export class NodeApiError extends NodeError {
httpCode: string | null;
constructor(
node: INode,
error: JsonObject,
{
message,
description,
httpCode,
parseXml,
runIndex,
itemIndex,
}: {
message?: string;
description?: string;
httpCode?: string;
parseXml?: boolean;
runIndex?: number;
itemIndex?: number;
} = {},
) {
super(node, error);
if (error.error) {
// only for request library error
this.removeCircularRefs(error.error as JsonObject);
}
// if it's an error generated by axios
// look for descriptions in the response object
if (error.isAxiosError && error.response) {
error = error.response as JsonObject;
}
if (message) {
this.message = message;
this.description = description;
this.httpCode = httpCode ?? null;
return;
}
this.httpCode = this.findProperty(error, ERROR_STATUS_PROPERTIES, ERROR_NESTING_PROPERTIES);
this.setMessage();
if (parseXml) {
this.setDescriptionFromXml(error.error as string);
return;
}
this.description = this.findProperty(error, ERROR_MESSAGE_PROPERTIES, ERROR_NESTING_PROPERTIES);
if (runIndex !== undefined) this.context.runIndex = runIndex;
if (itemIndex !== undefined) this.context.itemIndex = itemIndex;
}
private setDescriptionFromXml(xml: string) {
// eslint-disable-next-line @typescript-eslint/naming-convention
parseString(xml, { explicitArray: false }, (_, result) => {
if (!result) return;
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const topLevelKey = Object.keys(result)[0];
this.description = this.findProperty(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
result[topLevelKey],
ERROR_MESSAGE_PROPERTIES,
['Error'].concat(ERROR_NESTING_PROPERTIES),
);
});
}
/**
* Set the error's message based on the HTTP status code.
*
*/
private setMessage() {
if (!this.httpCode) {
this.httpCode = null;
this.message = UNKNOWN_ERROR_MESSAGE;
return;
}
if (STATUS_CODE_MESSAGES[this.httpCode]) {
this.message = STATUS_CODE_MESSAGES[this.httpCode];
return;
}
switch (this.httpCode.charAt(0)) {
case '4':
this.message = STATUS_CODE_MESSAGES['4XX'];
break;
case '5':
this.message = STATUS_CODE_MESSAGES['5XX'];
break;
default:
this.message = UNKNOWN_ERROR_MESSAGE;
}
}
}