import { PLACEHOLDER_FILLED_AT_EXECUTION_TIME, PLACEHOLDER_EMPTY_WORKFLOW_ID, WEBHOOK_NODE_TYPE, VIEWS, EnterpriseEditionFeature, } from '@/constants'; import type { IConnections, IDataObject, INode, INodeExecutionData, INodeIssues, INodeParameters, NodeParameterValue, INodeCredentials, INodeType, INodeTypes, IRunExecutionData, IWorkflowIssues, IWorkflowDataProxyAdditionalKeys, Workflow, IExecuteData, INodeConnection, IWebhookDescription, } from 'n8n-workflow'; import { NodeHelpers } from 'n8n-workflow'; import type { INodeTypesMaxCount, INodeUi, IWorkflowData, IWorkflowDb, IWorkflowDataUpdate, XYPosition, ITag, TargetItem, } from '../Interface'; import { externalHooks } from '@/mixins/externalHooks'; import { nodeHelpers } from '@/mixins/nodeHelpers'; import { showMessage } from '@/mixins/showMessage'; import { isEqual } from 'lodash-es'; import mixins from 'vue-typed-mixins'; import { v4 as uuid } from 'uuid'; import { getSourceItems } from '@/utils'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { useRootStore } from '@/stores/n8nRoot.store'; import type { IWorkflowSettings } from 'n8n-workflow'; import { useNDVStore } from '@/stores/ndv.store'; import { useTemplatesStore } from '@/stores/templates.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useWorkflowsEEStore } from '@/stores/workflows.ee.store'; import { useUsersStore } from '@/stores/users.store'; import type { IPermissions } from '@/permissions'; import { getWorkflowPermissions } from '@/permissions'; import type { ICredentialsResponse } from '@/Interface'; import { useEnvironmentsStore } from '@/stores'; export function resolveParameter( parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], opts: { targetItem?: TargetItem; inputNodeName?: string; inputRunIndex?: number; inputBranchIndex?: number; } = {}, ): IDataObject | null { let itemIndex = opts?.targetItem?.itemIndex || 0; const inputName = 'main'; const activeNode = useNDVStore().activeNode; const workflow = getCurrentWorkflow(); const workflowRunData = useWorkflowsStore().getWorkflowRunData; let parentNode = workflow.getParentNodes(activeNode!.name, inputName, 1); const executionData = useWorkflowsStore().getWorkflowExecution; let runIndexParent = opts?.inputRunIndex ?? 0; const nodeConnection = workflow.getNodeConnectionIndexes(activeNode!.name, parentNode[0]); if (opts.targetItem && opts?.targetItem?.nodeName === activeNode!.name && executionData) { const sourceItems = getSourceItems(executionData, opts.targetItem); if (!sourceItems.length) { return null; } parentNode = [sourceItems[0].nodeName]; runIndexParent = sourceItems[0].runIndex; itemIndex = sourceItems[0].itemIndex; if (nodeConnection) { nodeConnection.sourceIndex = sourceItems[0].outputIndex; } } else { parentNode = opts.inputNodeName ? [opts.inputNodeName] : parentNode; if (nodeConnection) { nodeConnection.sourceIndex = opts.inputBranchIndex ?? nodeConnection.sourceIndex; } if (opts?.inputRunIndex === undefined && workflowRunData !== null && parentNode.length) { const firstParentWithWorkflowRunData = parentNode.find( (parentNodeName) => workflowRunData[parentNodeName], ); if (firstParentWithWorkflowRunData) { runIndexParent = workflowRunData[firstParentWithWorkflowRunData].length - 1; } } } let _connectionInputData = connectionInputData( parentNode, activeNode!.name, inputName, runIndexParent, nodeConnection, ); let runExecutionData: IRunExecutionData; if (executionData === null || !executionData.data) { runExecutionData = { resultData: { runData: {}, }, }; } else { runExecutionData = executionData.data; } if (_connectionInputData === null) { _connectionInputData = []; } const additionalKeys: IWorkflowDataProxyAdditionalKeys = { $execution: { id: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, mode: 'test', resumeUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, }, $vars: useEnvironmentsStore().variablesAsObject, // deprecated $executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, $resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, }; let runIndexCurrent = opts?.targetItem?.runIndex ?? 0; if ( opts?.targetItem === undefined && workflowRunData !== null && workflowRunData[activeNode!.name] ) { runIndexCurrent = workflowRunData[activeNode!.name].length - 1; } const _executeData = executeData(parentNode, activeNode!.name, inputName, runIndexCurrent); return workflow.expression.getParameterValue( parameter, runExecutionData, runIndexCurrent, itemIndex, activeNode!.name, _connectionInputData, 'manual', useRootStore().timezone, additionalKeys, _executeData, false, ) as IDataObject; } function getCurrentWorkflow(copyData?: boolean): Workflow { return useWorkflowsStore().getCurrentWorkflow(copyData); } function getNodes(): INodeUi[] { return useWorkflowsStore().getNodes(); } // Returns a workflow instance. function getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow { return useWorkflowsStore().getWorkflow(nodes, connections, copyData); } function getNodeTypes(): INodeTypes { return useWorkflowsStore().getNodeTypes(); } // Returns connectionInputData to be able to execute an expression. function connectionInputData( parentNode: string[], currentNode: string, inputName: string, runIndex: number, nodeConnection: INodeConnection = { sourceIndex: 0, destinationIndex: 0 }, ): INodeExecutionData[] | null { let connectionInputData: INodeExecutionData[] | null = null; const _executeData = executeData(parentNode, currentNode, inputName, runIndex); if (parentNode.length) { if ( !Object.keys(_executeData.data).length || _executeData.data[inputName].length <= nodeConnection.sourceIndex ) { connectionInputData = []; } else { connectionInputData = _executeData.data![inputName][nodeConnection.sourceIndex]; if (connectionInputData !== null) { // Update the pairedItem information on items connectionInputData = connectionInputData.map((item, itemIndex) => { return { ...item, pairedItem: { item: itemIndex, input: nodeConnection.destinationIndex, }, }; }); } } } const parentPinData = parentNode.reduce((acc: INodeExecutionData[], parentNodeName, index) => { const pinData = useWorkflowsStore().pinDataByNodeName(parentNodeName); if (pinData) { acc.push({ json: pinData[0], pairedItem: { item: index, input: 1, }, }); } return acc; }, []); if (parentPinData.length > 0) { if (connectionInputData && connectionInputData.length > 0) { parentPinData.forEach((parentPinDataEntry) => { connectionInputData![0].json = { ...connectionInputData![0].json, ...parentPinDataEntry.json, }; }); } else { connectionInputData = parentPinData; } } return connectionInputData; } function executeData( parentNode: string[], currentNode: string, inputName: string, runIndex: number, ): IExecuteData { const executeData = { node: {}, data: {}, source: null, } as IExecuteData; if (parentNode.length) { // Add the input data to be able to also resolve the short expression format // which does not use the node name const parentNodeName = parentNode[0]; const parentPinData = useWorkflowsStore().getPinData![parentNodeName]; // populate `executeData` from `pinData` if (parentPinData) { executeData.data = { main: [parentPinData] }; executeData.source = { main: [{ previousNode: parentNodeName }] }; return executeData; } // populate `executeData` from `runData` const workflowRunData = useWorkflowsStore().getWorkflowRunData; if (workflowRunData === null) { return executeData; } if ( !workflowRunData[parentNodeName] || workflowRunData[parentNodeName].length <= runIndex || !workflowRunData[parentNodeName][runIndex] || !workflowRunData[parentNodeName][runIndex].hasOwnProperty('data') || workflowRunData[parentNodeName][runIndex].data === undefined || !workflowRunData[parentNodeName][runIndex].data!.hasOwnProperty(inputName) ) { executeData.data = {}; } else { executeData.data = workflowRunData[parentNodeName][runIndex].data!; if (workflowRunData[currentNode] && workflowRunData[currentNode][runIndex]) { executeData.source = { [inputName]: workflowRunData[currentNode][runIndex].source!, }; } else { // The current node did not get executed in UI yet so build data manually executeData.source = { [inputName]: [ { previousNode: parentNodeName, }, ], }; } } } return executeData; } export const workflowHelpers = mixins(externalHooks, nodeHelpers, showMessage).extend({ computed: { ...mapStores( useNodeTypesStore, useNDVStore, useRootStore, useTemplatesStore, useWorkflowsStore, useWorkflowsEEStore, useUsersStore, useUIStore, ), workflowPermissions(): IPermissions { return getWorkflowPermissions(this.usersStore.currentUser, this.workflowsStore.workflow); }, }, methods: { resolveParameter, getCurrentWorkflow, getNodes, getWorkflow, getNodeTypes, connectionInputData, executeData, // Returns data about nodeTypes which have a "maxNodes" limit set. // For each such type does it return how high the limit is, how many // already exist and the name of this nodes. getNodeTypesMaxCount(): INodeTypesMaxCount { const nodes = this.workflowsStore.allNodes; const returnData: INodeTypesMaxCount = {}; const nodeTypes = this.nodeTypesStore.allNodeTypes; for (const nodeType of nodeTypes) { if (nodeType.maxNodes !== undefined) { returnData[nodeType.name] = { exist: 0, max: nodeType.maxNodes, nodeNames: [], }; } } for (const node of nodes) { if (returnData[node.type] !== undefined) { returnData[node.type].exist += 1; returnData[node.type].nodeNames.push(node.name); } } return returnData; }, // Returns how many nodes of the given type currently exist getNodeTypeCount(nodeType: string): number { const nodes = this.workflowsStore.allNodes; let count = 0; for (const node of nodes) { if (node.type === nodeType) { count++; } } return count; }, // Checks if everything in the workflow is complete and ready to be executed checkReadyForExecution(workflow: Workflow, lastNodeName?: string) { let node: INode; let nodeType: INodeType | undefined; let nodeIssues: INodeIssues | null = null; const workflowIssues: IWorkflowIssues = {}; let checkNodes = Object.keys(workflow.nodes); if (lastNodeName) { checkNodes = workflow.getParentNodes(lastNodeName); checkNodes.push(lastNodeName); } else { // As webhook nodes always take precedence check first // if there are any let checkWebhook: string[] = []; for (const nodeName of Object.keys(workflow.nodes)) { if ( workflow.nodes[nodeName].disabled !== true && workflow.nodes[nodeName].type === WEBHOOK_NODE_TYPE ) { checkWebhook = [nodeName, ...checkWebhook, ...workflow.getChildNodes(nodeName)]; } } if (checkWebhook.length) { checkNodes = checkWebhook; } else { // If no webhook nodes got found try to find another trigger node const startNode = workflow.getStartNode(); if (startNode !== undefined) { checkNodes = workflow.getChildNodes(startNode.name); checkNodes.push(startNode.name); } } } for (const nodeName of checkNodes) { nodeIssues = null; node = workflow.nodes[nodeName]; if (node.disabled === true) { // Ignore issues on disabled nodes continue; } nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); if (nodeType === undefined) { // Node type is not known nodeIssues = { typeUnknown: true, }; } else { nodeIssues = this.getNodeIssues(nodeType.description, node, ['execution']); } if (nodeIssues !== null) { workflowIssues[node.name] = nodeIssues; } } if (Object.keys(workflowIssues).length === 0) { return null; } return workflowIssues; }, // Returns the currently loaded workflow as JSON. async getWorkflowDataToSave(): Promise { const workflowNodes = this.workflowsStore.allNodes; const workflowConnections = this.workflowsStore.allConnections; let nodeData; const nodes = []; for (let nodeIndex = 0; nodeIndex < workflowNodes.length; nodeIndex++) { // @ts-ignore nodeData = this.getNodeDataToSave(workflowNodes[nodeIndex]); nodes.push(nodeData); } const data: IWorkflowData = { name: this.workflowsStore.workflowName, nodes, pinData: this.workflowsStore.getPinData, connections: workflowConnections, active: this.workflowsStore.isWorkflowActive, settings: this.workflowsStore.workflow.settings, tags: this.workflowsStore.workflowTags, versionId: this.workflowsStore.workflow.versionId, }; const workflowId = this.workflowsStore.workflowId; if (workflowId !== PLACEHOLDER_EMPTY_WORKFLOW_ID) { data.id = workflowId; } return data; }, // Returns all node-types getNodeDataToSave(node: INodeUi): INodeUi { const skipKeys = [ 'color', 'continueOnFail', 'credentials', 'disabled', 'issues', 'notes', 'parameters', 'status', ]; // @ts-ignore const nodeData: INodeUi = { parameters: {}, }; for (const key in node) { if (key.charAt(0) !== '_' && skipKeys.indexOf(key) === -1) { // @ts-ignore nodeData[key] = node[key]; } } // Get the data of the node type that we can get the default values // TODO: Later also has to care about the node-type-version as defaults could be different const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion); if (nodeType !== null) { // Node-Type is known so we can save the parameters correctly const nodeParameters = NodeHelpers.getNodeParameters( nodeType.properties, node.parameters, false, false, node, ); nodeData.parameters = nodeParameters !== null ? nodeParameters : {}; // Add the node credentials if there are some set and if they should be displayed if (node.credentials !== undefined && nodeType.credentials !== undefined) { const saveCredentials: INodeCredentials = {}; for (const nodeCredentialTypeName of Object.keys(node.credentials)) { if ( this.hasProxyAuth(node) || Object.keys(node.parameters).includes('genericAuthType') ) { saveCredentials[nodeCredentialTypeName] = node.credentials[nodeCredentialTypeName]; continue; } const credentialTypeDescription = nodeType.credentials // filter out credentials with same name in different node versions .filter((c) => this.displayParameter(node.parameters, c, '', node)) .find((c) => c.name === nodeCredentialTypeName); if (credentialTypeDescription === undefined) { // Credential type is not know so do not save continue; } if ( this.displayParameter(node.parameters, credentialTypeDescription, '', node) === false ) { // Credential should not be displayed so do also not save continue; } saveCredentials[nodeCredentialTypeName] = node.credentials[nodeCredentialTypeName]; } // Set credential property only if it has content if (Object.keys(saveCredentials).length !== 0) { nodeData.credentials = saveCredentials; } } } else { // Node-Type is not known so save the data as it is nodeData.credentials = node.credentials; nodeData.parameters = node.parameters; if (nodeData.color !== undefined) { nodeData.color = node.color; } } // Save the disabled property and continueOnFail only when is set if (node.disabled === true) { nodeData.disabled = true; } if (node.continueOnFail === true) { nodeData.continueOnFail = true; } // Save the notes only if when they contain data if (![undefined, ''].includes(node.notes)) { nodeData.notes = node.notes; } return nodeData; }, getWebhookExpressionValue(webhookData: IWebhookDescription, key: string): string { if (webhookData[key] === undefined) { return 'empty'; } try { return this.resolveExpression(webhookData[key] as string) as string; } catch (e) { return this.$locale.baseText('nodeWebhooks.invalidExpression'); } }, getWebhookUrl(webhookData: IWebhookDescription, node: INode, showUrlFor?: string): string { if (webhookData.restartWebhook === true) { return '$execution.resumeUrl'; } let baseUrl = this.rootStore.getWebhookUrl; if (showUrlFor === 'test') { baseUrl = this.rootStore.getWebhookTestUrl; } const workflowId = this.workflowsStore.workflowId; const path = this.getWebhookExpressionValue(webhookData, 'path'); const isFullPath = (this.getWebhookExpressionValue(webhookData, 'isFullPath') as unknown as boolean) || false; return NodeHelpers.getNodeWebhookUrl(baseUrl, workflowId, node, path, isFullPath); }, resolveExpression( expression: string, siblingParameters: INodeParameters = {}, opts: { targetItem?: TargetItem; inputNodeName?: string; inputRunIndex?: number; inputBranchIndex?: number; c?: number; } = {}, ) { const parameters = { __xxxxxxx__: expression, ...siblingParameters, }; const returnData: IDataObject | null = resolveParameter(parameters, opts); if (!returnData) { return null; } if (typeof returnData['__xxxxxxx__'] === 'object') { const workflow = getCurrentWorkflow(); return workflow.expression.convertObjectValueToString(returnData['__xxxxxxx__'] as object); } return returnData['__xxxxxxx__']; }, async updateWorkflow({ workflowId, active }: { workflowId: string; active?: boolean }) { let data: IWorkflowDataUpdate = {}; const isCurrentWorkflow = workflowId === this.workflowsStore.workflowId; if (isCurrentWorkflow) { data = await this.getWorkflowDataToSave(); } else { const { versionId } = await this.workflowsStore.fetchWorkflow(workflowId); data.versionId = versionId; } if (active !== undefined) { data.active = active; } const workflow = await this.workflowsStore.updateWorkflow(workflowId, data); this.workflowsStore.setWorkflowVersionId(workflow.versionId); if (isCurrentWorkflow) { this.workflowsStore.setActive(!!workflow.active); this.uiStore.stateIsDirty = false; } if (workflow.active) { this.workflowsStore.setWorkflowActive(workflowId); } else { this.workflowsStore.setWorkflowInactive(workflowId); } }, async saveCurrentWorkflow( { id, name, tags }: { id?: string; name?: string; tags?: string[] } = {}, redirect = true, forceSave = false, ): Promise { const currentWorkflow = id || this.$route.params.name; if (!currentWorkflow || ['new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(currentWorkflow)) { return this.saveAsNewWorkflow({ name, tags }, redirect); } // Workflow exists already so update it try { this.uiStore.addActiveAction('workflowSaving'); const workflowDataRequest: IWorkflowDataUpdate = await this.getWorkflowDataToSave(); if (name) { workflowDataRequest.name = name.trim(); } if (tags) { workflowDataRequest.tags = tags; } workflowDataRequest.versionId = this.workflowsStore.workflowVersionId; const workflowData = await this.workflowsStore.updateWorkflow( currentWorkflow, workflowDataRequest, forceSave, ); this.workflowsStore.setWorkflowVersionId(workflowData.versionId); if (name) { this.workflowsStore.setWorkflowName({ newName: workflowData.name, setStateDirty: false }); } if (tags) { const createdTags = (workflowData.tags || []) as ITag[]; const tagIds = createdTags.map((tag: ITag): string => tag.id); this.workflowsStore.setWorkflowTagIds(tagIds); } this.uiStore.stateIsDirty = false; this.uiStore.removeActiveAction('workflowSaving'); this.$externalHooks().run('workflow.afterUpdate', { workflowData }); return true; } catch (error) { this.uiStore.removeActiveAction('workflowSaving'); if (error.errorCode === 100) { this.$telemetry.track('User attempted to save locked workflow', { workflowId: currentWorkflow, sharing_role: this.workflowPermissions.isOwner ? 'owner' : 'sharee', }); const url = this.$router.resolve({ name: VIEWS.WORKFLOW, params: { name: currentWorkflow }, }).href; const overwrite = await this.confirmMessage( this.$locale.baseText('workflows.concurrentChanges.confirmMessage.message', { interpolate: { url, }, }), this.$locale.baseText('workflows.concurrentChanges.confirmMessage.title'), null, this.$locale.baseText('workflows.concurrentChanges.confirmMessage.confirmButtonText'), this.$locale.baseText('workflows.concurrentChanges.confirmMessage.cancelButtonText'), ); if (overwrite) { return this.saveCurrentWorkflow({ id, name, tags }, redirect, true); } return false; } this.$showMessage({ title: this.$locale.baseText('workflowHelpers.showMessage.title'), message: error.message, type: 'error', }); return false; } }, async saveAsNewWorkflow( { name, tags, resetWebhookUrls, resetNodeIds, openInNewWindow, data, }: { name?: string; tags?: string[]; resetWebhookUrls?: boolean; openInNewWindow?: boolean; resetNodeIds?: boolean; data?: IWorkflowDataUpdate; } = {}, redirect = true, ): Promise { try { this.uiStore.addActiveAction('workflowSaving'); const workflowDataRequest: IWorkflowDataUpdate = data || (await this.getWorkflowDataToSave()); // make sure that the new ones are not active workflowDataRequest.active = false; const changedNodes = {} as IDataObject; if (resetNodeIds) { workflowDataRequest.nodes = workflowDataRequest.nodes!.map((node) => { node.id = uuid(); return node; }); } if (resetWebhookUrls) { workflowDataRequest.nodes = workflowDataRequest.nodes!.map((node) => { if (node.webhookId) { node.webhookId = uuid(); changedNodes[node.name] = node.webhookId; } return node; }); } if (name) { workflowDataRequest.name = name.trim(); } if (tags) { workflowDataRequest.tags = tags; } const workflowData = await this.workflowsStore.createNewWorkflow(workflowDataRequest); this.workflowsStore.addWorkflow(workflowData); if ( this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing) && this.usersStore.currentUser ) { this.workflowsEEStore.setWorkflowOwnedBy({ workflowId: workflowData.id, ownedBy: this.usersStore.currentUser, }); } if (openInNewWindow) { const routeData = this.$router.resolve({ name: VIEWS.WORKFLOW, params: { name: workflowData.id }, }); window.open(routeData.href, '_blank'); this.uiStore.removeActiveAction('workflowSaving'); return true; } this.workflowsStore.setActive(workflowData.active || false); this.workflowsStore.setWorkflowId(workflowData.id); this.workflowsStore.setWorkflowVersionId(workflowData.versionId); this.workflowsStore.setWorkflowName({ newName: workflowData.name, setStateDirty: false }); this.workflowsStore.setWorkflowSettings((workflowData.settings as IWorkflowSettings) || {}); this.uiStore.stateIsDirty = false; Object.keys(changedNodes).forEach((nodeName) => { const changes = { key: 'webhookId', value: changedNodes[nodeName], name: nodeName, }; this.workflowsStore.setNodeValue(changes); }); const createdTags = (workflowData.tags || []) as ITag[]; const tagIds = createdTags.map((tag: ITag): string => tag.id); this.workflowsStore.setWorkflowTagIds(tagIds); const templateId = this.$route.query.templateId; if (templateId) { this.$telemetry.track('User saved new workflow from template', { template_id: templateId, workflow_id: workflowData.id, wf_template_repo_session_id: this.templatesStore.previousSessionId, }); } if (redirect) { void this.$router.replace({ name: VIEWS.WORKFLOW, params: { name: workflowData.id as string, action: 'workflowSave' }, }); } this.uiStore.removeActiveAction('workflowSaving'); this.uiStore.stateIsDirty = false; this.$externalHooks().run('workflow.afterUpdate', { workflowData }); getCurrentWorkflow(true); // refresh cache return true; } catch (e) { this.uiStore.removeActiveAction('workflowSaving'); this.$showMessage({ title: this.$locale.baseText('workflowHelpers.showMessage.title'), message: (e as Error).message, type: 'error', }); return false; } }, // Updates the position of all the nodes that the top-left node // is at the given position updateNodePositions( workflowData: IWorkflowData | IWorkflowDataUpdate, position: XYPosition, ): void { if (workflowData.nodes === undefined) { return; } // Find most top-left node const minPosition = [99999999, 99999999]; for (const node of workflowData.nodes) { if (node.position[1] < minPosition[1]) { minPosition[0] = node.position[0]; minPosition[1] = node.position[1]; } else if (node.position[1] === minPosition[1]) { if (node.position[0] < minPosition[0]) { minPosition[0] = node.position[0]; minPosition[1] = node.position[1]; } } } // Update the position on all nodes so that the // most top-left one is at given position const offsetPosition = [position[0] - minPosition[0], position[1] - minPosition[1]]; for (const node of workflowData.nodes) { node.position[0] += offsetPosition[0]; node.position[1] += offsetPosition[1]; } }, async dataHasChanged(id: string) { const currentData = await this.getWorkflowDataToSave(); const data: IWorkflowDb = await this.workflowsStore.fetchWorkflow(id); if (data !== undefined) { const x = { nodes: data.nodes, connections: data.connections, settings: data.settings, name: data.name, }; const y = { nodes: currentData.nodes, connections: currentData.connections, settings: currentData.settings, name: currentData.name, }; return !isEqual(x, y); } return true; }, removeForeignCredentialsFromWorkflow( workflow: IWorkflowData | IWorkflowDataUpdate, usableCredentials: ICredentialsResponse[], ): void { workflow.nodes.forEach((node: INode) => { if (!node.credentials) { return; } node.credentials = Object.entries(node.credentials).reduce( (acc, [credentialType, credential]) => { const isUsableCredential = usableCredentials.some( (ownCredential) => `${ownCredential.id}` === `${credential.id}`, ); if (credential.id && isUsableCredential) { acc[credentialType] = node.credentials![credentialType]; } return acc; }, {}, ); }); }, }, });