refactor(core): Switch plain errors in core to ApplicationError (no-changelog) (#7873)

Ensure all errors in `core` are `ApplicationError` or children of it and
contain no variables in the message, to continue normalizing all the
errors we report to Sentry

Follow-up to: https://github.com/n8n-io/n8n/pull/7857
This commit is contained in:
Iván Ovejero 2023-11-30 09:06:19 +01:00 committed by GitHub
parent cd474f1562
commit b16dd21909
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 135 additions and 101 deletions

View file

@ -13,6 +13,7 @@ import type {
WorkflowExecuteMode,
} from 'n8n-workflow';
import {
ApplicationError,
LoggerProxy as Logger,
toCronExpression,
WorkflowActivationError,
@ -177,7 +178,9 @@ export class ActiveWorkflows {
for (const cronTime of cronTimes) {
const cronTimeParts = cronTime.split(' ');
if (cronTimeParts.length > 0 && cronTimeParts[0].includes('*')) {
throw new Error('The polling interval is too short. It has to be at least a minute!');
throw new ApplicationError(
'The polling interval is too short. It has to be at least a minute!',
);
}
cronJobs.push(new CronJob(cronTime, executeTrigger, undefined, true, timezone));

View file

@ -1,6 +1,6 @@
import { Container } from 'typedi';
import type { ICredentialDataDecryptedObject, ICredentialsEncrypted } from 'n8n-workflow';
import { ICredentials, jsonParse } from 'n8n-workflow';
import { ApplicationError, ICredentials, jsonParse } from 'n8n-workflow';
import { Cipher } from './Cipher';
export class Credentials extends ICredentials {
@ -31,13 +31,14 @@ export class Credentials extends ICredentials {
*/
getData(nodeType?: string): ICredentialDataDecryptedObject {
if (nodeType && !this.hasNodeAccess(nodeType)) {
throw new Error(
`The node of type "${nodeType}" does not have access to credentials "${this.name}" of type "${this.type}".`,
);
throw new ApplicationError('Node does not have access to credential', {
tags: { nodeType, credentialType: this.type },
extra: { credentialName: this.name },
});
}
if (this.data === undefined) {
throw new Error('No data is set so nothing can be returned.');
throw new ApplicationError('No data is set so nothing can be returned.');
}
const decryptedData = this.cipher.decrypt(this.data);
@ -45,7 +46,7 @@ export class Credentials extends ICredentials {
try {
return jsonParse(decryptedData);
} catch (e) {
throw new Error(
throw new ApplicationError(
'Credentials could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.',
);
}
@ -56,7 +57,7 @@ export class Credentials extends ICredentials {
*/
getDataToSave(): ICredentialsEncrypted {
if (this.data === undefined) {
throw new Error('No credentials were set to save.');
throw new ApplicationError('No credentials were set to save.');
}
return {

View file

@ -14,6 +14,7 @@ import type {
KnownNodesAndCredentials,
} from 'n8n-workflow';
import {
ApplicationError,
LoggerProxy as Logger,
getCredentialsForNode,
getVersionedNodeTypeAll,
@ -116,8 +117,9 @@ export abstract class DirectoryLoader {
nodeVersion = tempNode.currentVersion;
if (currentVersionNode.hasOwnProperty('executeSingle')) {
throw new Error(
`"executeSingle" has been removed. Please update the code of node "${this.packageName}.${nodeName}" to use "execute" instead!`,
throw new ApplicationError(
'"executeSingle" has been removed. Please update the code of this node to use "execute" instead!',
{ extra: { nodeName: `${this.packageName}.${nodeName}` } },
);
}
} else {
@ -156,10 +158,10 @@ export abstract class DirectoryLoader {
}
}
protected loadCredentialFromFile(credentialName: string, filePath: string): void {
protected loadCredentialFromFile(credentialClassName: string, filePath: string): void {
let tempCredential: ICredentialType;
try {
tempCredential = loadClassInIsolation(filePath, credentialName);
tempCredential = loadClassInIsolation(filePath, credentialClassName);
// Add serializer method "toJSON" to the class so that authenticate method (if defined)
// gets mapped to the authenticate attribute before it is sent to the client.
@ -170,8 +172,9 @@ export abstract class DirectoryLoader {
this.fixIconPath(tempCredential, filePath);
} catch (e) {
if (e instanceof TypeError) {
throw new Error(
`Class with name "${credentialName}" could not be found. Please check if the class is named correctly!`,
throw new ApplicationError(
'Class could not be found. Please check if the class is named correctly.',
{ extra: { credentialClassName } },
);
} else {
throw e;
@ -179,7 +182,7 @@ export abstract class DirectoryLoader {
}
this.known.credentials[tempCredential.name] = {
className: credentialName,
className: credentialClassName,
sourcePath: filePath,
extends: tempCredential.extends,
supportedNodes: this.nodesByCredential[tempCredential.name],
@ -366,7 +369,7 @@ export class PackageDirectoryLoader extends DirectoryLoader {
try {
return jsonParse<T>(fileString);
} catch (error) {
throw new Error(`Failed to parse JSON from ${filePath}`);
throw new ApplicationError('Failed to parse JSON', { extra: { filePath } });
}
}
}

View file

@ -7,7 +7,13 @@ import type {
INodeType,
NodeParameterValueType,
} from 'n8n-workflow';
import { NodeOperationError, NodeHelpers, LoggerProxy, WorkflowOperationError } from 'n8n-workflow';
import {
NodeOperationError,
NodeHelpers,
LoggerProxy,
WorkflowOperationError,
ApplicationError,
} from 'n8n-workflow';
function findPropertyFromParameterName(
parameterName: string,
@ -41,14 +47,14 @@ function findPropertyFromParameterName(
property = findProp(param, property.values);
currentParamPath += `.${param}`;
} else {
throw new Error(`Couldn't not find property "${parameterName}"`);
throw new ApplicationError('Could not find property', { extra: { parameterName } });
}
if (!property) {
throw new Error(`Couldn't not find property "${parameterName}"`);
throw new ApplicationError('Could not find property', { extra: { parameterName } });
}
}
if (!property) {
throw new Error(`Couldn't not find property "${parameterName}"`);
throw new ApplicationError('Could not find property', { extra: { parameterName } });
}
return property;
@ -101,17 +107,16 @@ function extractValueRLC(
LoggerProxy.error(
`Only strings can be passed to extractValue. Parameter "${parameterName}" passed "${typeName}"`,
);
throw new Error(
`ERROR: ${property.displayName} parameter's value is invalid. Please enter a valid ${modeProp.displayName}.`,
throw new ApplicationError(
"ERROR: This parameter's value is invalid. Please enter a valid mode.",
{ extra: { parameter: property.displayName, modeProp: modeProp.displayName } },
);
}
if (modeProp.extractValue.type !== 'regex') {
throw new Error(
`Property "${parameterName}" has an unknown extractValue type "${
modeProp.extractValue.type as string
}"`,
);
throw new ApplicationError('Property with unknown `extractValue`', {
extra: { parameter: parameterName, extractValueType: modeProp.extractValue.type },
});
}
const regex = new RegExp(modeProp.extractValue.regex);
@ -137,17 +142,15 @@ function extractValueOther(
LoggerProxy.error(
`Only strings can be passed to extractValue. Parameter "${parameterName}" passed "${typeName}"`,
);
throw new Error(
`ERROR: ${property.displayName} parameter's value is invalid. Please enter a valid value.`,
);
throw new ApplicationError("This parameter's value is invalid", {
extra: { parameter: property.displayName },
});
}
if (property.extractValue.type !== 'regex') {
throw new Error(
`Property "${parameterName}" has an unknown extractValue type "${
property.extractValue.type as string
}"`,
);
throw new ApplicationError('Property with unknown `extractValue`', {
extra: { parameter: parameterName, extractValueType: property.extractValue.type },
});
}
const regex = new RegExp(property.extractValue.regex);

View file

@ -112,6 +112,7 @@ import {
validateFieldType,
ExecutionBaseError,
jsonParse,
ApplicationError,
} from 'n8n-workflow';
import type { Token } from 'oauth-1.0a';
import clientOAuth1 from 'oauth-1.0a';
@ -1198,7 +1199,7 @@ export async function requestOAuth2(
credentials.grantType === OAuth2GrantType.authorizationCode &&
credentials.oauthTokenData === undefined
) {
throw new Error('OAuth credentials not connected!');
throw new ApplicationError('OAuth credentials not connected');
}
const oAuthClient = new ClientOAuth2({
@ -1218,9 +1219,10 @@ export async function requestOAuth2(
const { data } = await getClientCredentialsToken(oAuthClient, credentials);
// Find the credentials
if (!node.credentials?.[credentialsType]) {
throw new Error(
`The node "${node.name}" does not have credentials of type "${credentialsType}"!`,
);
throw new ApplicationError('Node does not have credential type', {
extra: { nodeName: node.name },
tags: { credentialType: credentialsType },
});
}
const nodeCredentials = node.credentials[credentialsType];
@ -1302,9 +1304,9 @@ export async function requestOAuth2(
credentials.oauthTokenData = newToken.data;
// Find the credentials
if (!node.credentials?.[credentialsType]) {
throw new Error(
`The node "${node.name}" does not have credentials of type "${credentialsType}"!`,
);
throw new ApplicationError('Node does not have credential type', {
extra: { nodeName: node.name, credentialType: credentialsType },
});
}
const nodeCredentials = node.credentials[credentialsType];
await additionalData.credentialsHelper.updateCredentials(
@ -1379,9 +1381,10 @@ export async function requestOAuth2(
// Find the credentials
if (!node.credentials?.[credentialsType]) {
throw new Error(
`The node "${node.name}" does not have credentials of type "${credentialsType}"!`,
);
throw new ApplicationError('Node does not have credential type', {
tags: { credentialType: credentialsType },
extra: { nodeName: node.name },
});
}
const nodeCredentials = node.credentials[credentialsType];
@ -1426,11 +1429,11 @@ export async function requestOAuth1(
const credentials = await this.getCredentials(credentialsType);
if (credentials === undefined) {
throw new Error('No credentials were returned!');
throw new ApplicationError('No credentials were returned!');
}
if (credentials.oauthTokenData === undefined) {
throw new Error('OAuth credentials not connected!');
throw new ApplicationError('OAuth credentials not connected!');
}
const oauth = new clientOAuth1({
@ -1651,7 +1654,7 @@ export function normalizeItems(
return executionData;
if (executionData.some((item) => typeof item === 'object' && 'json' in item)) {
throw new Error('Inconsistent item format');
throw new ApplicationError('Inconsistent item format');
}
if (executionData.every((item) => typeof item === 'object' && 'binary' in item)) {
@ -1671,7 +1674,7 @@ export function normalizeItems(
}
if (executionData.some((item) => typeof item === 'object' && 'binary' in item)) {
throw new Error('Inconsistent item format');
throw new ApplicationError('Inconsistent item format');
}
return executionData.map((item) => {
@ -2229,13 +2232,15 @@ export function getNodeParameter(
): NodeParameterValueType | object {
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
throw new Error(`Node type "${node.type}" is not known so can not return parameter value!`);
throw new ApplicationError('Node type is unknown so cannot return parameter value', {
tags: { nodeType: node.type },
});
}
const value = get(node.parameters, parameterName, fallbackValue);
if (value === undefined) {
throw new Error(`Could not get parameter "${parameterName}"!`);
throw new ApplicationError('Could not get parameter', { extra: { parameterName } });
}
if (options?.rawExpressions) {
@ -2393,7 +2398,9 @@ const addExecutionDataFunctions = async (
currentNodeRunIndex: number,
): Promise<void> => {
if (connectionType === 'main') {
throw new Error(`Setting the ${type} is not supported for the main connection!`);
throw new ApplicationError('Setting type is not supported for main connection', {
extra: { type },
});
}
let taskData: ITaskData | undefined;
@ -2946,7 +2953,7 @@ const getBinaryHelperFunctions = (
setBinaryDataBuffer: async (data, binaryData) =>
setBinaryDataBuffer(data, binaryData, workflowId, executionId!),
copyBinaryFile: async () => {
throw new Error('copyBinaryFile has been removed. Please upgrade this node');
throw new ApplicationError('`copyBinaryFile` has been removed. Please upgrade this node.');
},
});
@ -2983,10 +2990,12 @@ export function getExecutePollFunctions(
return {
...getCommonWorkflowFunctions(workflow, node, additionalData),
__emit: (data: INodeExecutionData[][]): void => {
throw new Error('Overwrite NodeExecuteFunctions.getExecutePollFunctions.__emit function!');
throw new ApplicationError(
'Overwrite NodeExecuteFunctions.getExecutePollFunctions.__emit function!',
);
},
__emitError(error: Error) {
throw new Error(
throw new ApplicationError(
'Overwrite NodeExecuteFunctions.getExecutePollFunctions.__emitError function!',
);
},
@ -3043,10 +3052,14 @@ export function getExecuteTriggerFunctions(
return {
...getCommonWorkflowFunctions(workflow, node, additionalData),
emit: (data: INodeExecutionData[][]): void => {
throw new Error('Overwrite NodeExecuteFunctions.getExecuteTriggerFunctions.emit function!');
throw new ApplicationError(
'Overwrite NodeExecuteFunctions.getExecuteTriggerFunctions.emit function!',
);
},
emitError: (error: Error): void => {
throw new Error('Overwrite NodeExecuteFunctions.getExecuteTriggerFunctions.emit function!');
throw new ApplicationError(
'Overwrite NodeExecuteFunctions.getExecuteTriggerFunctions.emit function!',
);
},
getMode: () => mode,
getActivationMode: () => activation,
@ -3174,7 +3187,9 @@ export function getExecuteFunctions(
});
if (inputConfiguration === undefined) {
throw new Error(`The node "${node.name}" does not have an input of type "${inputName}"`);
throw new ApplicationError('Node does not have input of type', {
extra: { nodeName: node.name, inputName },
});
}
if (typeof inputConfiguration === 'string') {
@ -3200,9 +3215,9 @@ export function getExecuteFunctions(
);
if (!nodeType.supplyData) {
throw new Error(
`The node "${connectedNode.name}" does not have a "supplyData" method defined!`,
);
throw new ApplicationError('Node does not have a `supplyData` method defined', {
extra: { nodeName: connectedNode.name },
});
}
const context = Object.assign({}, this);
@ -3350,12 +3365,15 @@ export function getExecuteFunctions(
// TODO: Check if nodeType has input with that index defined
if (inputData[inputName].length < inputIndex) {
throw new Error(`Could not get input index "${inputIndex}" of input "${inputName}"!`);
throw new ApplicationError('Could not get input with given index', {
extra: { inputIndex, inputName },
});
}
if (inputData[inputName][inputIndex] === null) {
// return [];
throw new Error(`Value "${inputIndex}" of input "${inputName}" did not get set!`);
throw new ApplicationError('Value of input was not set', {
extra: { inputIndex, inputName },
});
}
return inputData[inputName][inputIndex] as INodeExecutionData[];
@ -3363,7 +3381,7 @@ export function getExecuteFunctions(
getInputSourceData: (inputIndex = 0, inputName = 'main') => {
if (executeData?.source === null) {
// Should never happen as n8n sets it automatically
throw new Error('Source data is missing!');
throw new ApplicationError('Source data is missing');
}
return executeData.source[inputName][inputIndex];
},
@ -3573,21 +3591,23 @@ export function getExecuteSingleFunctions(
// TODO: Check if nodeType has input with that index defined
if (inputData[inputName].length < inputIndex) {
throw new Error(`Could not get input index "${inputIndex}" of input "${inputName}"!`);
throw new ApplicationError('Could not get input index', {
extra: { inputIndex, inputName },
});
}
const allItems = inputData[inputName][inputIndex];
if (allItems === null) {
// return [];
throw new Error(`Value "${inputIndex}" of input "${inputName}" did not get set!`);
throw new ApplicationError('Input index was not set', {
extra: { inputIndex, inputName },
});
}
if (allItems[itemIndex] === null) {
// return [];
throw new Error(
`Value "${inputIndex}" of input "${inputName}" with itemIndex "${itemIndex}" did not get set!`,
);
throw new ApplicationError('Value of input with given index was not set', {
extra: { inputIndex, inputName, itemIndex },
});
}
return allItems[itemIndex];
@ -3595,7 +3615,7 @@ export function getExecuteSingleFunctions(
getInputSourceData: (inputIndex = 0, inputName = 'main') => {
if (executeData?.source === null) {
// Should never happen as n8n sets it automatically
throw new Error('Source data is missing!');
throw new ApplicationError('Source data is missing');
}
return executeData.source[inputName][inputIndex] as ISourceData;
},
@ -3691,9 +3711,9 @@ export function getLoadOptionsFunctions(
if (options?.extractValue) {
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) {
throw new Error(
`Node type "${node.type}" is not known so can not return parameter value!`,
);
throw new ApplicationError('Node type is not known so cannot return parameter value', {
tags: { nodeType: node.type },
});
}
returnData = extractValue(
returnData,
@ -3793,7 +3813,7 @@ export function getExecuteHookFunctions(
},
getWebhookName(): string {
if (webhookData === undefined) {
throw new Error('Is only supported in webhook functions!');
throw new ApplicationError('Only supported in webhook functions');
}
return webhookData.webhookDescription.name;
},
@ -3818,14 +3838,14 @@ export function getExecuteWebhookFunctions(
...getCommonWorkflowFunctions(workflow, node, additionalData),
getBodyData(): IDataObject {
if (additionalData.httpRequest === undefined) {
throw new Error('Request is missing!');
throw new ApplicationError('Request is missing');
}
return additionalData.httpRequest.body;
},
getCredentials: async (type) => getCredentials(workflow, node, type, additionalData, mode),
getHeaderData(): IncomingHttpHeaders {
if (additionalData.httpRequest === undefined) {
throw new Error('Request is missing!');
throw new ApplicationError('Request is missing');
}
return additionalData.httpRequest.headers;
},
@ -3857,25 +3877,25 @@ export function getExecuteWebhookFunctions(
},
getParamsData(): object {
if (additionalData.httpRequest === undefined) {
throw new Error('Request is missing!');
throw new ApplicationError('Request is missing');
}
return additionalData.httpRequest.params;
},
getQueryData(): object {
if (additionalData.httpRequest === undefined) {
throw new Error('Request is missing!');
throw new ApplicationError('Request is missing');
}
return additionalData.httpRequest.query;
},
getRequestObject(): Request {
if (additionalData.httpRequest === undefined) {
throw new Error('Request is missing!');
throw new ApplicationError('Request is missing');
}
return additionalData.httpRequest;
},
getResponseObject(): Response {
if (additionalData.httpResponse === undefined) {
throw new Error('Response is missing!');
throw new ApplicationError('Response is missing');
}
return additionalData.httpResponse;
},

View file

@ -5,7 +5,7 @@ import axios from 'axios';
import { Service } from 'typedi';
import { sign } from 'aws4';
import { isStream, parseXml, writeBlockedMessage } from './utils';
import { LoggerProxy as Logger } from 'n8n-workflow';
import { ApplicationError, LoggerProxy as Logger } from 'n8n-workflow';
import type { AxiosRequestConfig, AxiosResponse, Method } from 'axios';
import type { Request as Aws4Options, Credentials as Aws4Credentials } from 'aws4';
@ -283,7 +283,7 @@ export class ObjectStoreService {
this.logger.error(message, { config });
throw new Error(message, { cause: { error, details: config } });
throw new ApplicationError(message, { cause: error, extra: { config } });
}
}
}

View file

@ -38,6 +38,7 @@ import {
WorkflowOperationError,
NodeHelpers,
NodeConnectionType,
ApplicationError,
} from 'n8n-workflow';
import get from 'lodash/get';
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
@ -89,7 +90,7 @@ export class WorkflowExecute {
startNode = startNode || workflow.getStartNode(destinationNode);
if (startNode === undefined) {
throw new Error('No node to start the workflow from could be found!');
throw new ApplicationError('No node to start the workflow from could be found');
}
// If a destination node is given we only run the direct parent nodes and no others
@ -929,7 +930,9 @@ export class WorkflowExecute {
currentExecutionTry = `${executionNode.name}:${runIndex}`;
if (currentExecutionTry === lastExecutionTry) {
throw new Error('Did stop execution because execution seems to be in endless loop.');
throw new ApplicationError(
'Stopped execution because it seems to be in an endless loop',
);
}
if (
@ -1412,9 +1415,12 @@ export class WorkflowExecute {
outputIndex
]) {
if (!workflow.nodes.hasOwnProperty(connectionData.node)) {
throw new Error(
`The node "${executionNode.name}" connects to not found node "${connectionData.node}"`,
);
throw new ApplicationError('Destination node not found', {
extra: {
sourceNodeName: executionNode.name,
destinationNodeName: connectionData.node,
},
});
}
if (

View file

@ -84,9 +84,7 @@ describe('Credentials', () => {
credentials.getData('base.otherNode');
expect(true).toBe(false);
} catch (e) {
expect(e.message).toBe(
'The node of type "base.otherNode" does not have access to credentials "testName" of type "testType".',
);
expect(e.message).toBe('Node does not have access to credential');
}
// Get the data which will be saved in database

View file

@ -1,5 +1,5 @@
import type { IRun, WorkflowTestData } from 'n8n-workflow';
import { createDeferredPromise, Workflow } from 'n8n-workflow';
import { ApplicationError, createDeferredPromise, Workflow } from 'n8n-workflow';
import { WorkflowExecute } from '@/WorkflowExecute';
import * as Helpers from './helpers';
@ -45,7 +45,7 @@ describe('WorkflowExecute', () => {
// Check if the output data of the nodes is correct
for (const nodeName of Object.keys(testData.output.nodeData)) {
if (result.data.resultData.runData[nodeName] === undefined) {
throw new Error(`Data for node "${nodeName}" is missing!`);
throw new ApplicationError('Data for node is missing', { extra: { nodeName } });
}
const resultData = result.data.resultData.runData[nodeName].map((nodeData) => {
@ -108,7 +108,7 @@ describe('WorkflowExecute', () => {
// Check if the output data of the nodes is correct
for (const nodeName of Object.keys(testData.output.nodeData)) {
if (result.data.resultData.runData[nodeName] === undefined) {
throw new Error(`Data for node "${nodeName}" is missing!`);
throw new ApplicationError('Data for node is missing', { extra: { nodeName } });
}
const resultData = result.data.resultData.runData[nodeName].map((nodeData) => {
@ -172,7 +172,7 @@ describe('WorkflowExecute', () => {
// Check if the output data of the nodes is correct
for (const nodeName of Object.keys(testData.output.nodeData)) {
if (result.data.resultData.runData[nodeName] === undefined) {
throw new Error(`Data for node "${nodeName}" is missing!`);
throw new ApplicationError('Data for node is missing', { extra: { nodeName } });
}
const resultData = result.data.resultData.runData[nodeName].map((nodeData) => {

View file

@ -24,7 +24,7 @@ import type {
INodeTypeData,
} from 'n8n-workflow';
import { ICredentialsHelper, NodeHelpers, WorkflowHooks } from 'n8n-workflow';
import { ApplicationError, ICredentialsHelper, NodeHelpers, WorkflowHooks } from 'n8n-workflow';
import { Credentials } from '@/Credentials';
import { predefinedNodesTypes } from './constants';
@ -177,11 +177,11 @@ export function getNodeTypes(testData: WorkflowTestData[] | WorkflowTestData) {
for (const nodeName of nodeNames) {
if (!nodeName.startsWith('n8n-nodes-base.')) {
throw new Error(`Unknown node type: ${nodeName}`);
throw new ApplicationError('Unknown node type', { tags: { nodeType: nodeName } });
}
const loadInfo = knownNodes[nodeName.replace('n8n-nodes-base.', '')];
if (!loadInfo) {
throw new Error(`Unknown node type: ${nodeName}`);
throw new ApplicationError('Unknown node type', { tags: { nodeType: nodeName } });
}
const sourcePath = loadInfo.sourcePath.replace(/^dist\//, './').replace(/\.js$/, '.ts');
const nodeSourcePath = path.join(BASE_DIR, 'nodes-base', sourcePath);
@ -218,7 +218,7 @@ export const workflowToTests = (dirname: string, testFolder = 'workflows') => {
const description = filePath.replace('.json', '');
const workflowData = readJsonFileSync<IWorkflowBase>(filePath);
if (workflowData.pinData === undefined) {
throw new Error('Workflow data does not contain pinData');
throw new ApplicationError('Workflow data does not contain pinData');
}
const nodeData = preparePinData(workflowData.pinData);