* 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`
363 lines
9.8 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|