diff --git a/packages/nodes-base/nodes/DebugHelper/DebugHelper.node.json b/packages/nodes-base/nodes/DebugHelper/DebugHelper.node.json new file mode 100644 index 0000000000..526841464a --- /dev/null +++ b/packages/nodes-base/nodes/DebugHelper/DebugHelper.node.json @@ -0,0 +1,10 @@ +{ + "node": "n8n-nodes-base.debughelper", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Development"], + "resources": { + "credentialDocumentation": [], + "primaryDocumentation": [] + } +} diff --git a/packages/nodes-base/nodes/DebugHelper/DebugHelper.node.ts b/packages/nodes-base/nodes/DebugHelper/DebugHelper.node.ts new file mode 100644 index 0000000000..df502803bd --- /dev/null +++ b/packages/nodes-base/nodes/DebugHelper/DebugHelper.node.ts @@ -0,0 +1,374 @@ +import type { + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; +import { NodeApiError, NodeOperationError } from 'n8n-workflow'; +import { + generateCreditCard, + generateIPv4, + generateIPv6, + generateLocation, + generateMAC, + generateNanoid, + generateRandomAddress, + generateRandomEmail, + generateRandomUser, + generateURL, + generateUUID, + generateVersion, +} from './randomData'; +import { setSeed, array as mfArray } from 'minifaker'; +import { generateGarbageMemory, runGarbageCollector } from './functions'; + +export class DebugHelper implements INodeType { + description: INodeTypeDescription = { + displayName: 'DebugHelper', + name: 'debugHelper', + icon: 'file:DebugHelper.svg', + group: ['output'], + subtitle: '={{$parameter["category"]}}', + description: 'Causes problems intentionally and generates useful data for debugging', + version: 1, + defaults: { + name: 'DebugHelper', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [], + properties: [ + { + displayName: 'Category', + name: 'category', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Do Nothing', + value: 'doNothing', + description: 'Does nothing', + }, + { + name: 'Throw Error', + value: 'throwError', + description: 'Throws an error with the specified type and message', + }, + { + name: 'Out Of Memory', + value: 'oom', + description: 'Generates a large amount of memory to cause an out of memory error', + }, + { + name: 'Generate Random Data', + value: 'randomData', + description: 'Generates random data sets', + }, + ], + default: 'throwError', + }, + { + displayName: 'Error Type', + name: 'throwErrorType', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'NodeApiError', + value: 'NodeApiError', + }, + { + name: 'NodeOperationError', + value: 'NodeOperationError', + }, + { + name: 'Error', + value: 'Error', + }, + ], + default: 'NodeApiError', + displayOptions: { + show: { + category: ['throwError'], + }, + }, + }, + { + displayName: 'Error Message', + name: 'throwErrorMessage', + type: 'string', + default: 'Node has thrown an error', + description: 'The message to send as part of the error', + displayOptions: { + show: { + category: ['throwError'], + }, + }, + }, + { + displayName: 'Memory Size to Generate', + name: 'memorySizeValue', + type: 'number', + default: 10, + description: 'The approximate amount of memory to generate. Be generous...', + displayOptions: { + show: { + category: ['oom'], + }, + }, + }, + { + displayName: 'Data Type', + name: 'randomDataType', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Address', + value: 'address', + }, + { + name: 'Coordinates', + value: 'latLong', + }, + { + name: 'Credit Card', + value: 'creditCard', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'IPv4', + value: 'ipv4', + }, + { + name: 'IPv6', + value: 'ipv6', + }, + { + name: 'MAC', + value: 'macAddress', + }, + { + name: 'NanoIds', + value: 'nanoid', + }, + { + name: 'URL', + value: 'url', + }, + { + name: 'User Data', + value: 'user', + }, + { + name: 'UUID', + value: 'uuid', + }, + { + name: 'Version', + value: 'semver', + }, + ], + default: 'user', + displayOptions: { + show: { + category: ['randomData'], + }, + }, + }, + { + displayName: 'NanoId Alphabet', + name: 'nanoidAlphabet', + type: 'string', + default: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + description: 'The alphabet to use for generating the nanoIds', + displayOptions: { + show: { + category: ['randomData'], + randomDataType: ['nanoid'], + }, + }, + }, + { + displayName: 'NanoId Length', + name: 'nanoidLength', + type: 'string', + default: '16', + description: 'The length of each nanoIds', + displayOptions: { + show: { + category: ['randomData'], + randomDataType: ['nanoid'], + }, + }, + }, + { + displayName: 'Seed', + name: 'randomDataSeed', + type: 'string', + default: '', + placeholder: 'Leave empty for random seed', + description: + 'If set, seed to use for generating the data (same seed will generate the same data)', + displayOptions: { + show: { + category: ['randomData'], + }, + }, + }, + { + displayName: 'Number of Items to Generate', + name: 'randomDataCount', + type: 'number', + default: 10, + description: 'The number of random data items to generate into an array', + displayOptions: { + show: { + category: ['randomData'], + }, + }, + }, + { + displayName: 'Output as Single Array', + name: 'randomDataSingleArray', + type: 'boolean', + default: false, + description: 'Whether to output a single array instead of multiple items', + displayOptions: { + show: { + category: ['randomData'], + }, + }, + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + const category = this.getNodeParameter('category', 0) as string; + + for (let i = 0; i < items.length; i++) { + try { + switch (category) { + case 'doNothing': + // as it says on the tin... + break; + case 'throwError': + const throwErrorType = this.getNodeParameter('throwErrorType', 0) as string; + const throwErrorMessage = this.getNodeParameter('throwErrorMessage', 0) as string; + switch (throwErrorType) { + case 'NodeApiError': + throw new NodeApiError( + this.getNode(), + { message: throwErrorMessage }, + { message: throwErrorMessage }, + ); + case 'NodeOperationError': + throw new NodeOperationError(this.getNode(), throwErrorMessage, { + message: throwErrorMessage, + }); + case 'Error': + // eslint-disable-next-line n8n-nodes-base/node-execute-block-wrong-error-thrown + throw new Error(throwErrorMessage); + default: + break; + } + case 'oom': + const memorySizeValue = this.getNodeParameter('memorySizeValue', 0) as number; + runGarbageCollector(); + const memUsed = generateGarbageMemory(memorySizeValue); + items[i].json = memUsed; + returnData.push(items[i]); + break; + case 'randomData': + const randomDataType = this.getNodeParameter('randomDataType', 0) as string; + const randomDataCount = this.getNodeParameter('randomDataCount', 0) as number; + const randomDataSeed = this.getNodeParameter('randomDataSeed', 0) as string; + const randomDataSingleArray = this.getNodeParameter( + 'randomDataSingleArray', + 0, + ) as boolean; + const newItem: INodeExecutionData = { + json: {}, + pairedItem: { item: i }, + }; + if (randomDataSeed !== '') { + setSeed(randomDataSeed); + } + + let randomFn: () => any = generateRandomUser; + switch (randomDataType) { + case 'user': + randomFn = generateRandomUser; + break; + case 'email': + randomFn = generateRandomEmail; + break; + case 'address': + randomFn = generateRandomAddress; + break; + case 'creditCard': + randomFn = generateCreditCard; + break; + case 'uuid': + randomFn = generateUUID; + break; + case 'macAddress': + randomFn = generateMAC; + break; + case 'ipv4': + randomFn = generateIPv4; + break; + case 'ipv6': + randomFn = generateIPv6; + break; + case 'latLong': + randomFn = generateLocation; + break; + case 'semver': + randomFn = generateVersion; + break; + case 'url': + randomFn = generateURL; + break; + case 'nanoid': + const nanoidAlphabet = this.getNodeParameter('nanoidAlphabet', 0) as string; + const nanoidLength = this.getNodeParameter('nanoidLength', 0) as string; + randomFn = () => generateNanoid(nanoidAlphabet, nanoidLength); + break; + } + const generatedItems = mfArray(randomDataCount, randomFn); + if (randomDataSingleArray) { + newItem.json = { generatedItems }; + returnData.push(newItem); + } else { + for (const generatedItem of generatedItems) { + returnData.push({ + json: generatedItem, + pairedItem: { item: i }, + }); + } + } + break; + default: + break; + } + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; + } + } + return this.prepareOutputData(returnData); + } +} diff --git a/packages/nodes-base/nodes/DebugHelper/DebugHelper.svg b/packages/nodes-base/nodes/DebugHelper/DebugHelper.svg new file mode 100644 index 0000000000..0a63b95e52 --- /dev/null +++ b/packages/nodes-base/nodes/DebugHelper/DebugHelper.svg @@ -0,0 +1,28 @@ + + + + + + + diff --git a/packages/nodes-base/nodes/DebugHelper/functions.ts b/packages/nodes-base/nodes/DebugHelper/functions.ts new file mode 100644 index 0000000000..a740b577b7 --- /dev/null +++ b/packages/nodes-base/nodes/DebugHelper/functions.ts @@ -0,0 +1,30 @@ +import { setFlagsFromString } from 'v8'; +import { runInNewContext } from 'vm'; + +export const runGarbageCollector = () => { + try { + setFlagsFromString('--expose_gc'); + const gc = runInNewContext('gc'); // nocommit + gc(); + } catch (error) { + console.log(error); + } +}; + +export const generateGarbageMemory = (sizeInMB: number, onHeap = true) => { + const divider = onHeap ? 8 : 1; + const size = Math.max(1, Math.floor((sizeInMB * 1024 * 1024) / divider)); + if (onHeap) { + // arrays are allocated on the heap + // size in this case is only an approximation... + const array = Array(size); + array.fill(0); + } else { + const array = new Uint8Array(size); + array.fill(0); + } + // const used = process.memoryUsage().heapUsed / 1024 / 1024; + // const external = process.memoryUsage().external / 1024 / 1024; + // console.log(`heap: ${used} MB / external: ${external} MB`); + return { ...process.memoryUsage() }; +}; diff --git a/packages/nodes-base/nodes/DebugHelper/randomData.ts b/packages/nodes-base/nodes/DebugHelper/randomData.ts new file mode 100644 index 0000000000..14234e6fb0 --- /dev/null +++ b/packages/nodes-base/nodes/DebugHelper/randomData.ts @@ -0,0 +1,101 @@ +import { + firstName, + lastName, + streetAddress, + cityName, + zipCode, + state, + country, + password, + creditCardNumber, + creditCardCVV, + email, + boolean, + uuid, + nanoId, + domainUrl, + semver, + latLong, + macAddress, + ip, + ipv6, + number, +} from 'minifaker'; +import 'minifaker/locales/en'; + +export function generateRandomUser() { + return { + uid: uuid.v4(), + email: email(), + firstname: firstName(), + lastname: lastName(), + password: password(), + }; +} + +export function generateRandomAddress() { + return { + firstname: firstName(), + lastname: lastName(), + street: streetAddress(), + city: cityName(), + zip: zipCode({ format: '#####' }), + state: state(), + country: country(), + }; +} + +export function generateRandomEmail() { + return { + email: email(), + confirmed: boolean(), + }; +} + +export function generateUUID() { + return { uuid: uuid.v4() }; +} + +export function generateNanoid(customAlphabet: string, length: string) { + return { nanoId: nanoId.customAlphabet(customAlphabet, parseInt(length, 10))().toString() }; +} + +export function generateCreditCard() { + return { + type: boolean() ? 'MasterCard' : 'Visa', + number: creditCardNumber(), + ccv: creditCardCVV(), + exp: `${number({ min: 1, max: 12, float: false }).toString().padStart(2, '0')}/${number({ + min: 1, + max: 40, + float: false, + }) + .toString() + .padStart(2, '0')}`, + holder_name: `${firstName()} ${lastName()}`, + }; +} + +export function generateURL() { + return { url: domainUrl() }; +} + +export function generateIPv4() { + return { ip: ip() }; +} + +export function generateIPv6() { + return { ipv6: ipv6() }; +} + +export function generateMAC() { + return { mac: macAddress() }; +} + +export function generateLocation() { + return { location: latLong() }; +} + +export function generateVersion() { + return { version: semver() }; +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index e96c7ab07d..f4b52092a6 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -424,6 +424,7 @@ "dist/nodes/CustomerIo/CustomerIo.node.js", "dist/nodes/CustomerIo/CustomerIoTrigger.node.js", "dist/nodes/DateTime/DateTime.node.js", + "dist/nodes/DebugHelper/DebugHelper.node.js", "dist/nodes/DeepL/DeepL.node.js", "dist/nodes/Demio/Demio.node.js", "dist/nodes/Dhl/Dhl.node.js", @@ -806,6 +807,7 @@ "lossless-json": "^1.0.4", "luxon": "^3.3.0", "mailparser": "^3.2.0", + "minifaker": "^1.34.1", "moment": "~2.29.2", "moment-timezone": "^0.5.28", "mongodb": "^4.9.1", @@ -813,6 +815,7 @@ "mssql": "^8.1.2", "mysql2": "~2.3.0", "n8n-workflow": "workspace:*", + "nanoid": "^3.3.6", "node-html-markdown": "^1.1.3", "node-ssh": "^12.0.0", "nodemailer": "^6.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 982fd0a6ff..3860b3e636 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1087,6 +1087,9 @@ importers: mailparser: specifier: ^3.2.0 version: 3.5.0 + minifaker: + specifier: ^1.34.1 + version: 1.34.1 moment: specifier: ~2.29.2 version: 2.29.4 @@ -1108,6 +1111,9 @@ importers: n8n-workflow: specifier: workspace:* version: link:../workflow + nanoid: + specifier: ^3.3.6 + version: 3.3.6 node-html-markdown: specifier: ^1.1.3 version: 1.2.0 @@ -6734,7 +6740,7 @@ packages: fetch-retry: 5.0.3 fs-extra: 11.1.0 isomorphic-unfetch: 3.1.0 - nanoid: 3.3.4 + nanoid: 3.3.6 read-pkg-up: 7.0.1 transitivePeerDependencies: - encoding @@ -7647,7 +7653,6 @@ packages: /@types/uuid@8.3.4: resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} - dev: true /@types/uuid@9.0.0: resolution: {integrity: sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==} @@ -14028,7 +14033,7 @@ packages: /ics@2.40.0: resolution: {integrity: sha512-PPkE9ij60sGhqdTxZZzsXQPB/TCXAB/dD3NqUf1I/GkbJzPeJHHMzaoMQiYAsm1pFaHRp2OIhFDgUBihkk8s/w==} dependencies: - nanoid: 3.3.4 + nanoid: 3.3.6 yup: 0.32.11 dev: false @@ -16575,6 +16580,14 @@ packages: engines: {node: '>=4'} dev: true + /minifaker@1.34.1: + resolution: {integrity: sha512-O9+c6GaUETgtKe65bJkpDTJxGcAALiUPqJtDv97dT3o0uP2HmyUVEguEGm6PLKuoSzZUmHqSTZ4cS7m8xKFEAg==} + dependencies: + '@types/uuid': 8.3.4 + nanoid: 3.3.6 + uuid: 8.3.2 + dev: false + /minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} dev: false @@ -16933,8 +16946,8 @@ packages: resolution: {integrity: sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==} dev: false - /nanoid@3.3.4: - resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} + /nanoid@3.3.6: + resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -18241,7 +18254,7 @@ packages: resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} engines: {node: ^10 || ^12 || >=14} dependencies: - nanoid: 3.3.4 + nanoid: 3.3.6 picocolors: 1.0.0 source-map-js: 1.0.2