n8n/packages/nodes-base/utils/utilities.ts
Iván Ovejero e77fd5d286
refactor: Switch plain errors in nodes-base to ApplicationError (no-changelog) (#7914)
Ensure all errors in `nodes-base` are `ApplicationError` or children of
it and contain no variables in the message, to continue normalizing all
the backend errors we report to Sentry. Also, skip reporting to Sentry
errors from user input and from external APIs. In future we should
refine `ApplicationError` to more specific errors.

Follow-up to: [#7877](https://github.com/n8n-io/n8n/pull/7877)

- [x] Test workflows:
https://github.com/n8n-io/n8n/actions/runs/7084627970
- [x] e2e: https://github.com/n8n-io/n8n/actions/runs/7084936861

---------

Co-authored-by: Michael Kret <michael.k@radency.com>
2023-12-05 11:17:08 +01:00

329 lines
8.9 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 type {
IDataObject,
IDisplayOptions,
INodeExecutionData,
INodeProperties,
IPairedItemData,
} from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import { isEqual, isNull, merge } from 'lodash';
/**
* Creates an array of elements split into groups the length of `size`.
* If `array` can't be split evenly, the final chunk will be the remaining
* elements.
*
* @param {Array} array The array to process.
* @param {number} [size=1] The length of each chunk
* @example
*
* chunk(['a', 'b', 'c', 'd'], 2)
* // => [['a', 'b'], ['c', 'd']]
*
* chunk(['a', 'b', 'c', 'd'], 3)
* // => [['a', 'b', 'c'], ['d']]
*/
export function chunk<T>(array: T[], size = 1) {
const length = array === null ? 0 : array.length;
if (!length || size < 1) {
return [];
}
let index = 0;
let resIndex = 0;
const result = new Array(Math.ceil(length / size));
while (index < length) {
result[resIndex++] = array.slice(index, (index += size));
}
return result as T[][];
}
/**
* Takes a multidimensional array and converts it to a one-dimensional array.
*
* @param {Array} nestedArray The array to be flattened.
* @example
*
* flatten([['a', 'b'], ['c', 'd']])
* // => ['a', 'b', 'c', 'd']
*
*/
export function flatten<T>(nestedArray: T[][]) {
const result = [];
(function loop(array: T[] | T[][]) {
for (let i = 0; i < array.length; i++) {
if (Array.isArray(array[i])) {
loop(array[i] as T[]);
} else {
result.push(array[i]);
}
}
})(nestedArray);
//TODO: check logic in MicrosoftSql.node.ts
return result as any;
}
export function updateDisplayOptions(
displayOptions: IDisplayOptions,
properties: INodeProperties[],
) {
return properties.map((nodeProperty) => {
return {
...nodeProperty,
displayOptions: merge({}, nodeProperty.displayOptions, displayOptions),
};
});
}
export function processJsonInput<T>(jsonData: T, inputName?: string) {
let values;
const input = inputName ? `'${inputName}' ` : '';
if (typeof jsonData === 'string') {
try {
values = jsonParse(jsonData);
} catch (error) {
throw new ApplicationError(`Input ${input} must contain a valid JSON`, { level: 'warning' });
}
} else if (typeof jsonData === 'object') {
values = jsonData;
} else {
throw new ApplicationError(`Input ${input} must contain a valid JSON`, { level: 'warning' });
}
return values;
}
function isFalsy<T>(value: T) {
if (isNull(value)) return true;
if (typeof value === 'string' && value === '') return true;
if (Array.isArray(value) && value.length === 0) return true;
return false;
}
const parseStringAndCompareToObject = (str: string, arr: IDataObject) => {
try {
const parsedArray = jsonParse(str);
return isEqual(parsedArray, arr);
} catch (error) {
return false;
}
};
export const fuzzyCompare = (useFuzzyCompare: boolean, compareVersion = 1) => {
if (!useFuzzyCompare) {
//Fuzzy compare is false we do strict comparison
return <T, U>(item1: T, item2: U) => isEqual(item1, item2);
}
return <T, U>(item1: T, item2: U) => {
//Both types are the same, so we do strict comparison
if (!isNull(item1) && !isNull(item2) && typeof item1 === typeof item2) {
return isEqual(item1, item2);
}
if (compareVersion >= 2) {
//Null, 0 and "0" treated as equal
if (isNull(item1) && (isNull(item2) || item2 === 0 || item2 === '0')) {
return true;
}
if (isNull(item2) && (isNull(item1) || item1 === 0 || item1 === '0')) {
return true;
}
}
//Null, empty strings, empty arrays all treated as the same
if (isFalsy(item1) && isFalsy(item2)) return true;
//When a field is missing in one branch and isFalsy() in another, treat them as matching
if (isFalsy(item1) && item2 === undefined) return true;
if (item1 === undefined && isFalsy(item2)) return true;
//Compare numbers and strings representing that number
if (typeof item1 === 'number' && typeof item2 === 'string') {
return item1.toString() === item2;
}
if (typeof item1 === 'string' && typeof item2 === 'number') {
return item1 === item2.toString();
}
//Compare objects/arrays and their stringified version
if (!isNull(item1) && typeof item1 === 'object' && typeof item2 === 'string') {
return parseStringAndCompareToObject(item2, item1 as IDataObject);
}
if (!isNull(item2) && typeof item1 === 'string' && typeof item2 === 'object') {
return parseStringAndCompareToObject(item1, item2 as IDataObject);
}
//Compare booleans and strings representing the boolean (true, True, TRUE)
if (typeof item1 === 'boolean' && typeof item2 === 'string') {
if (item1 === true && item2.toLocaleLowerCase() === 'true') return true;
if (item1 === false && item2.toLocaleLowerCase() === 'false') return true;
}
if (typeof item2 === 'boolean' && typeof item1 === 'string') {
if (item2 === true && item1.toLocaleLowerCase() === 'true') return true;
if (item2 === false && item1.toLocaleLowerCase() === 'false') return true;
}
//Compare booleans and the numbers/string 0 and 1
if (typeof item1 === 'boolean' && typeof item2 === 'number') {
if (item1 === true && item2 === 1) return true;
if (item1 === false && item2 === 0) return true;
}
if (typeof item2 === 'boolean' && typeof item1 === 'number') {
if (item2 === true && item1 === 1) return true;
if (item2 === false && item1 === 0) return true;
}
if (typeof item1 === 'boolean' && typeof item2 === 'string') {
if (item1 === true && item2 === '1') return true;
if (item1 === false && item2 === '0') return true;
}
if (typeof item2 === 'boolean' && typeof item1 === 'string') {
if (item2 === true && item1 === '1') return true;
if (item2 === false && item1 === '0') return true;
}
return isEqual(item1, item2);
};
};
export function wrapData(data: IDataObject | IDataObject[]): INodeExecutionData[] {
if (!Array.isArray(data)) {
return [{ json: data }];
}
return data.map((item) => ({
json: item,
}));
}
export const keysToLowercase = <T>(headers: T) => {
if (typeof headers !== 'object' || Array.isArray(headers) || headers === null) return headers;
return Object.entries(headers).reduce((acc, [key, value]) => {
acc[key.toLowerCase()] = value as IDataObject;
return acc;
}, {} as IDataObject);
};
/**
* Formats a private key by removing unnecessary whitespace and adding line breaks.
* @param privateKey - The private key to format.
* @returns The formatted private key.
*/
export function formatPrivateKey(privateKey: string): string {
if (!privateKey || /\n/.test(privateKey)) {
return privateKey;
}
let formattedPrivateKey = '';
const parts = privateKey.split('-----').filter((item) => item !== '');
parts.forEach((part) => {
const regex = /(PRIVATE KEY|CERTIFICATE)/;
if (regex.test(part)) {
formattedPrivateKey += `-----${part}-----`;
} else {
const passRegex = /Proc-Type|DEK-Info/;
if (passRegex.test(part)) {
part = part.replace(/:\s+/g, ':');
formattedPrivateKey += part.replace(/\\n/g, '\n').replace(/\s+/g, '\n');
} else {
formattedPrivateKey += part.replace(/\\n/g, '\n').replace(/\s+/g, '\n');
}
}
});
return formattedPrivateKey;
}
/**
* @TECH_DEBT Explore replacing with handlebars
*/
export function getResolvables(expression: string) {
if (!expression) return [];
const resolvables = [];
const resolvableRegex = /({{[\s\S]*?}})/g;
let match;
while ((match = resolvableRegex.exec(expression)) !== null) {
if (match[1]) {
resolvables.push(match[1]);
}
}
return resolvables;
}
/**
* Flattens an object with deep data
*
* @param {IDataObject} data The object to flatten
*/
export function flattenObject(data: IDataObject) {
const returnData: IDataObject = {};
for (const key1 of Object.keys(data)) {
if (data[key1] !== null && typeof data[key1] === 'object') {
if (data[key1] instanceof Date) {
returnData[key1] = data[key1]?.toString();
continue;
}
const flatObject = flattenObject(data[key1] as IDataObject);
for (const key2 in flatObject) {
if (flatObject[key2] === undefined) {
continue;
}
returnData[`${key1}.${key2}`] = flatObject[key2];
}
} else {
returnData[key1] = data[key1];
}
}
return returnData;
}
/**
* Capitalizes the first letter of a string
*
* @param {string} string The string to capitalize
*/
export function capitalize(str: string): string {
if (!str) return str;
const chars = str.split('');
chars[0] = chars[0].toUpperCase();
return chars.join('');
}
export function generatePairedItemData(length: number): IPairedItemData[] {
return Array.from({ length }, (_, item) => ({
item,
}));
}
/**
* Output Paired Item Data Array
*
* @param {number | IPairedItemData | IPairedItemData[] | undefined} pairedItem
*/
export function preparePairedItemDataArray(
pairedItem: number | IPairedItemData | IPairedItemData[] | undefined,
): IPairedItemData[] {
if (pairedItem === undefined) return [];
if (typeof pairedItem === 'number') return [{ item: pairedItem }];
if (Array.isArray(pairedItem)) return pairedItem;
return [pairedItem];
}