mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
1310 lines
36 KiB
TypeScript
1310 lines
36 KiB
TypeScript
import { ref, nextTick } from 'vue';
|
|
import { useRoute } from 'vue-router';
|
|
import type { Connection, ConnectionDetachedParams } from '@jsplumb/core';
|
|
import { useHistoryStore } from '@/stores/history.store';
|
|
import {
|
|
CUSTOM_API_CALL_KEY,
|
|
FORM_TRIGGER_NODE_TYPE,
|
|
NODE_OUTPUT_DEFAULT_KEY,
|
|
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
|
SPLIT_IN_BATCHES_NODE_TYPE,
|
|
WEBHOOK_NODE_TYPE,
|
|
} from '@/constants';
|
|
|
|
import { NodeHelpers, ExpressionEvaluatorProxy, NodeConnectionType } from 'n8n-workflow';
|
|
import type {
|
|
INodeProperties,
|
|
INodeCredentialDescription,
|
|
INodeTypeDescription,
|
|
INodeIssues,
|
|
ICredentialType,
|
|
INodeIssueObjectProperty,
|
|
INodeInputConfiguration,
|
|
Workflow,
|
|
INodeExecutionData,
|
|
ITaskDataConnections,
|
|
IRunData,
|
|
IBinaryKeyData,
|
|
IDataObject,
|
|
INode,
|
|
INodePropertyOptions,
|
|
INodeCredentialsDetails,
|
|
INodeParameters,
|
|
ITaskData,
|
|
IConnections,
|
|
INodeTypeNameVersion,
|
|
IConnection,
|
|
IPinData,
|
|
} from 'n8n-workflow';
|
|
|
|
import type {
|
|
ICredentialsResponse,
|
|
INodeUi,
|
|
INodeUpdatePropertiesInformation,
|
|
NodePanelType,
|
|
} from '@/Interface';
|
|
|
|
import { isString } from '@/utils/typeGuards';
|
|
import { isObject } from '@/utils/objectUtils';
|
|
import { useSettingsStore } from '@/stores/settings.store';
|
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
|
import { get } from 'lodash-es';
|
|
import { useI18n } from './useI18n';
|
|
import { AddNodeCommand, EnableNodeToggleCommand, RemoveConnectionCommand } from '@/models/history';
|
|
import { useTelemetry } from './useTelemetry';
|
|
import { hasPermission } from '@/utils/rbac/permissions';
|
|
import type { N8nPlusEndpoint } from '@/plugins/jsplumb/N8nPlusEndpointType';
|
|
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
|
import { useCanvasStore } from '@/stores/canvas.store';
|
|
import { getEndpointScope } from '@/utils/nodeViewUtils';
|
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
|
import { getConnectionInfo } from '@/utils/canvasUtils';
|
|
import type { UnpinNodeDataEvent } from '@/event-bus/data-pinning';
|
|
|
|
declare namespace HttpRequestNode {
|
|
namespace V2 {
|
|
type AuthParams = {
|
|
authentication: 'none' | 'genericCredentialType' | 'predefinedCredentialType';
|
|
genericAuthType: string;
|
|
nodeCredentialType: string;
|
|
};
|
|
}
|
|
}
|
|
|
|
export function useNodeHelpers() {
|
|
const credentialsStore = useCredentialsStore();
|
|
const historyStore = useHistoryStore();
|
|
const nodeTypesStore = useNodeTypesStore();
|
|
const workflowsStore = useWorkflowsStore();
|
|
const i18n = useI18n();
|
|
const canvasStore = useCanvasStore();
|
|
const sourceControlStore = useSourceControlStore();
|
|
const route = useRoute();
|
|
|
|
const isInsertingNodes = ref(false);
|
|
const credentialsUpdated = ref(false);
|
|
const isProductionExecutionPreview = ref(false);
|
|
const pullConnActiveNodeName = ref<string | null>(null);
|
|
|
|
function hasProxyAuth(node: INodeUi): boolean {
|
|
return Object.keys(node.parameters).includes('nodeCredentialType');
|
|
}
|
|
|
|
function isCustomApiCallSelected(nodeValues: INodeParameters): boolean {
|
|
const { parameters } = nodeValues;
|
|
|
|
if (!isObject(parameters)) return false;
|
|
|
|
if ('resource' in parameters || 'operation' in parameters) {
|
|
const { resource, operation } = parameters;
|
|
|
|
return (
|
|
(isString(resource) && resource.includes(CUSTOM_API_CALL_KEY)) ||
|
|
(isString(operation) && operation.includes(CUSTOM_API_CALL_KEY))
|
|
);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function getParameterValue(nodeValues: INodeParameters, parameterName: string, path: string) {
|
|
return get(nodeValues, path ? path + '.' + parameterName : parameterName);
|
|
}
|
|
|
|
// Returns if the given parameter should be displayed or not
|
|
function displayParameter(
|
|
nodeValues: INodeParameters,
|
|
parameter: INodeProperties | INodeCredentialDescription,
|
|
path: string,
|
|
node: INodeUi | null,
|
|
displayKey: 'displayOptions' | 'disabledOptions' = 'displayOptions',
|
|
) {
|
|
return NodeHelpers.displayParameterPath(nodeValues, parameter, path, node, displayKey);
|
|
}
|
|
|
|
function refreshNodeIssues(): void {
|
|
const nodes = workflowsStore.allNodes;
|
|
const workflow = workflowsStore.getCurrentWorkflow();
|
|
let nodeType: INodeTypeDescription | null;
|
|
let foundNodeIssues: INodeIssues | null;
|
|
|
|
nodes.forEach((node) => {
|
|
if (node.disabled === true) return;
|
|
|
|
nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
|
foundNodeIssues = getNodeIssues(nodeType, node, workflow);
|
|
if (foundNodeIssues !== null) {
|
|
node.issues = foundNodeIssues;
|
|
}
|
|
});
|
|
}
|
|
|
|
function getNodeIssues(
|
|
nodeType: INodeTypeDescription | null,
|
|
node: INodeUi,
|
|
workflow: Workflow,
|
|
ignoreIssues?: string[],
|
|
): INodeIssues | null {
|
|
const pinDataNodeNames = Object.keys(workflowsStore.pinnedWorkflowData ?? {});
|
|
|
|
let nodeIssues: INodeIssues | null = null;
|
|
ignoreIssues = ignoreIssues ?? [];
|
|
|
|
if (node.disabled === true || pinDataNodeNames.includes(node.name)) {
|
|
// Ignore issues on disabled and pindata nodes
|
|
return null;
|
|
}
|
|
|
|
if (nodeType === null) {
|
|
// Node type is not known
|
|
if (!ignoreIssues.includes('typeUnknown')) {
|
|
nodeIssues = {
|
|
typeUnknown: true,
|
|
};
|
|
}
|
|
} else {
|
|
// Node type is known
|
|
|
|
// Add potential parameter issues
|
|
if (!ignoreIssues.includes('parameters')) {
|
|
nodeIssues = NodeHelpers.getNodeParametersIssues(nodeType.properties, node);
|
|
}
|
|
|
|
if (!ignoreIssues.includes('credentials')) {
|
|
// Add potential credential issues
|
|
const nodeCredentialIssues = getNodeCredentialIssues(node, nodeType);
|
|
if (nodeIssues === null) {
|
|
nodeIssues = nodeCredentialIssues;
|
|
} else {
|
|
NodeHelpers.mergeIssues(nodeIssues, nodeCredentialIssues);
|
|
}
|
|
}
|
|
|
|
const nodeInputIssues = getNodeInputIssues(workflow, node, nodeType);
|
|
if (nodeIssues === null) {
|
|
nodeIssues = nodeInputIssues;
|
|
} else {
|
|
NodeHelpers.mergeIssues(nodeIssues, nodeInputIssues);
|
|
}
|
|
}
|
|
|
|
if (hasNodeExecutionIssues(node) && !ignoreIssues.includes('execution')) {
|
|
if (nodeIssues === null) {
|
|
nodeIssues = {};
|
|
}
|
|
nodeIssues.execution = true;
|
|
}
|
|
|
|
return nodeIssues;
|
|
}
|
|
|
|
// Set the status on all the nodes which produced an error so that it can be
|
|
// displayed in the node-view
|
|
function hasNodeExecutionIssues(node: INodeUi): boolean {
|
|
const workflowResultData = workflowsStore.getWorkflowRunData;
|
|
|
|
if (workflowResultData === null || !workflowResultData.hasOwnProperty(node.name)) {
|
|
return false;
|
|
}
|
|
|
|
for (const taskData of workflowResultData[node.name]) {
|
|
if (taskData.error !== undefined) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function reportUnsetCredential(credentialType: ICredentialType) {
|
|
return {
|
|
credentials: {
|
|
[credentialType.name]: [
|
|
i18n.baseText('nodeHelpers.credentialsUnset', {
|
|
interpolate: {
|
|
credentialType: credentialType.displayName,
|
|
},
|
|
}),
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
function updateNodeInputIssues(node: INodeUi): void {
|
|
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
|
if (!nodeType) {
|
|
return;
|
|
}
|
|
|
|
const workflow = workflowsStore.getCurrentWorkflow();
|
|
const nodeInputIssues = getNodeInputIssues(workflow, node, nodeType);
|
|
|
|
workflowsStore.setNodeIssue({
|
|
node: node.name,
|
|
type: 'input',
|
|
value: nodeInputIssues?.input ? nodeInputIssues.input : null,
|
|
});
|
|
}
|
|
|
|
function updateNodesInputIssues() {
|
|
const nodes = workflowsStore.allNodes;
|
|
|
|
for (const node of nodes) {
|
|
updateNodeInputIssues(node);
|
|
}
|
|
}
|
|
|
|
function updateNodesExecutionIssues() {
|
|
const nodes = workflowsStore.allNodes;
|
|
|
|
for (const node of nodes) {
|
|
workflowsStore.setNodeIssue({
|
|
node: node.name,
|
|
type: 'execution',
|
|
value: hasNodeExecutionIssues(node) ? true : null,
|
|
});
|
|
}
|
|
}
|
|
|
|
function updateNodesParameterIssues() {
|
|
const nodes = workflowsStore.allNodes;
|
|
|
|
for (const node of nodes) {
|
|
updateNodeParameterIssues(node);
|
|
}
|
|
}
|
|
|
|
function updateNodeCredentialIssuesByName(name: string): void {
|
|
const node = workflowsStore.getNodeByName(name);
|
|
|
|
if (node) {
|
|
updateNodeCredentialIssues(node);
|
|
}
|
|
}
|
|
|
|
function updateNodeCredentialIssues(node: INodeUi): void {
|
|
const fullNodeIssues: INodeIssues | null = getNodeCredentialIssues(node);
|
|
|
|
let newIssues: INodeIssueObjectProperty | null = null;
|
|
if (fullNodeIssues !== null) {
|
|
newIssues = fullNodeIssues.credentials!;
|
|
}
|
|
|
|
workflowsStore.setNodeIssue({
|
|
node: node.name,
|
|
type: 'credentials',
|
|
value: newIssues,
|
|
});
|
|
}
|
|
|
|
function updateNodeParameterIssuesByName(name: string): void {
|
|
const node = workflowsStore.getNodeByName(name);
|
|
|
|
if (node) {
|
|
updateNodeParameterIssues(node);
|
|
}
|
|
}
|
|
|
|
function updateNodeParameterIssues(node: INodeUi, nodeType?: INodeTypeDescription | null): void {
|
|
const localNodeType = nodeType ?? nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
|
|
|
if (localNodeType === null) {
|
|
// Could not find localNodeType so can not update issues
|
|
return;
|
|
}
|
|
|
|
// All data got updated everywhere so update now the issues
|
|
const fullNodeIssues: INodeIssues | null = NodeHelpers.getNodeParametersIssues(
|
|
localNodeType.properties,
|
|
node,
|
|
);
|
|
|
|
let newIssues: INodeIssueObjectProperty | null = null;
|
|
if (fullNodeIssues !== null) {
|
|
newIssues = fullNodeIssues.parameters!;
|
|
}
|
|
|
|
workflowsStore.setNodeIssue({
|
|
node: node.name,
|
|
type: 'parameters',
|
|
value: newIssues,
|
|
});
|
|
}
|
|
|
|
function getNodeInputIssues(
|
|
workflow: Workflow,
|
|
node: INodeUi,
|
|
nodeType?: INodeTypeDescription,
|
|
): INodeIssues | null {
|
|
const foundIssues: INodeIssueObjectProperty = {};
|
|
|
|
const workflowNode = workflow.getNode(node.name);
|
|
let inputs: Array<NodeConnectionType | INodeInputConfiguration> = [];
|
|
if (nodeType && workflowNode) {
|
|
inputs = NodeHelpers.getNodeInputs(workflow, workflowNode, nodeType);
|
|
}
|
|
|
|
inputs.forEach((input) => {
|
|
if (typeof input === 'string' || input.required !== true) {
|
|
return;
|
|
}
|
|
|
|
const parentNodes = workflow.getParentNodes(node.name, input.type, 1);
|
|
|
|
if (parentNodes.length === 0) {
|
|
foundIssues[input.type] = [
|
|
i18n.baseText('nodeIssues.input.missing', {
|
|
interpolate: { inputName: input.displayName || input.type },
|
|
}),
|
|
];
|
|
}
|
|
});
|
|
|
|
if (Object.keys(foundIssues).length) {
|
|
return {
|
|
input: foundIssues,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getNodeCredentialIssues(
|
|
node: INodeUi,
|
|
nodeType?: INodeTypeDescription,
|
|
): INodeIssues | null {
|
|
const localNodeType = nodeType ?? nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
|
if (node.disabled) {
|
|
// Node is disabled
|
|
return null;
|
|
}
|
|
if (!localNodeType?.credentials) {
|
|
// Node does not need any credentials or nodeType could not be found
|
|
return null;
|
|
}
|
|
|
|
const foundIssues: INodeIssueObjectProperty = {};
|
|
|
|
let userCredentials: ICredentialsResponse[] | null;
|
|
let credentialType: ICredentialType | undefined;
|
|
let credentialDisplayName: string;
|
|
let selectedCredentials: INodeCredentialsDetails;
|
|
|
|
const { authentication, genericAuthType, nodeCredentialType } =
|
|
node.parameters as HttpRequestNode.V2.AuthParams;
|
|
|
|
if (
|
|
authentication === 'genericCredentialType' &&
|
|
genericAuthType !== '' &&
|
|
selectedCredsAreUnusable(node, genericAuthType)
|
|
) {
|
|
const credential = credentialsStore.getCredentialTypeByName(genericAuthType);
|
|
return credential ? reportUnsetCredential(credential) : null;
|
|
}
|
|
|
|
if (
|
|
hasProxyAuth(node) &&
|
|
authentication === 'predefinedCredentialType' &&
|
|
nodeCredentialType !== '' &&
|
|
node.credentials !== undefined
|
|
) {
|
|
const stored = credentialsStore.getCredentialsByType(nodeCredentialType);
|
|
// Prevents HTTP Request node from being unusable if a sharee does not have direct
|
|
// access to a credential
|
|
const isCredentialUsedInWorkflow =
|
|
workflowsStore.usedCredentials?.[node.credentials?.[nodeCredentialType]?.id as string];
|
|
|
|
if (
|
|
selectedCredsDoNotExist(node, nodeCredentialType, stored) &&
|
|
!isCredentialUsedInWorkflow
|
|
) {
|
|
const credential = credentialsStore.getCredentialTypeByName(nodeCredentialType);
|
|
return credential ? reportUnsetCredential(credential) : null;
|
|
}
|
|
}
|
|
|
|
if (
|
|
hasProxyAuth(node) &&
|
|
authentication === 'predefinedCredentialType' &&
|
|
nodeCredentialType !== '' &&
|
|
selectedCredsAreUnusable(node, nodeCredentialType)
|
|
) {
|
|
const credential = credentialsStore.getCredentialTypeByName(nodeCredentialType);
|
|
return credential ? reportUnsetCredential(credential) : null;
|
|
}
|
|
|
|
for (const credentialTypeDescription of localNodeType.credentials) {
|
|
// Check if credentials should be displayed else ignore
|
|
if (!displayParameter(node.parameters, credentialTypeDescription, '', node)) {
|
|
continue;
|
|
}
|
|
|
|
// Get the display name of the credential type
|
|
credentialType = credentialsStore.getCredentialTypeByName(credentialTypeDescription.name);
|
|
if (!credentialType) {
|
|
credentialDisplayName = credentialTypeDescription.name;
|
|
} else {
|
|
credentialDisplayName = credentialType.displayName;
|
|
}
|
|
|
|
if (!node.credentials?.[credentialTypeDescription.name]) {
|
|
// Credentials are not set
|
|
if (credentialTypeDescription.required) {
|
|
foundIssues[credentialTypeDescription.name] = [
|
|
i18n.baseText('nodeIssues.credentials.notSet', {
|
|
interpolate: { type: localNodeType.displayName },
|
|
}),
|
|
];
|
|
}
|
|
} else {
|
|
// If they are set check if the value is valid
|
|
selectedCredentials = node.credentials[credentialTypeDescription.name];
|
|
if (typeof selectedCredentials === 'string') {
|
|
selectedCredentials = {
|
|
id: null,
|
|
name: selectedCredentials,
|
|
};
|
|
}
|
|
|
|
userCredentials = credentialsStore.getCredentialsByType(credentialTypeDescription.name);
|
|
|
|
if (userCredentials === null) {
|
|
userCredentials = [];
|
|
}
|
|
|
|
if (selectedCredentials.id) {
|
|
const idMatch = userCredentials.find(
|
|
(credentialData) => credentialData.id === selectedCredentials.id,
|
|
);
|
|
if (idMatch) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const nameMatches = userCredentials.filter(
|
|
(credentialData) => credentialData.name === selectedCredentials.name,
|
|
);
|
|
if (nameMatches.length > 1) {
|
|
foundIssues[credentialTypeDescription.name] = [
|
|
i18n.baseText('nodeIssues.credentials.notIdentified', {
|
|
interpolate: { name: selectedCredentials.name, type: credentialDisplayName },
|
|
}),
|
|
i18n.baseText('nodeIssues.credentials.notIdentified.hint'),
|
|
];
|
|
continue;
|
|
}
|
|
|
|
if (nameMatches.length === 0) {
|
|
const isCredentialUsedInWorkflow =
|
|
workflowsStore.usedCredentials?.[selectedCredentials.id as string];
|
|
|
|
if (
|
|
!isCredentialUsedInWorkflow &&
|
|
!hasPermission(['rbac'], { rbac: { scope: 'credential:read' } })
|
|
) {
|
|
foundIssues[credentialTypeDescription.name] = [
|
|
i18n.baseText('nodeIssues.credentials.doNotExist', {
|
|
interpolate: { name: selectedCredentials.name, type: credentialDisplayName },
|
|
}),
|
|
i18n.baseText('nodeIssues.credentials.doNotExist.hint'),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Could later check also if the node has access to the credentials
|
|
if (Object.keys(foundIssues).length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
credentials: foundIssues,
|
|
};
|
|
}
|
|
/**
|
|
* Whether the node has no selected credentials, or none of the node's
|
|
* selected credentials are of the specified type.
|
|
*/
|
|
function selectedCredsAreUnusable(node: INodeUi, credentialType: string) {
|
|
return !node.credentials || !Object.keys(node.credentials).includes(credentialType);
|
|
}
|
|
|
|
/**
|
|
* Whether the node's selected credentials of the specified type
|
|
* can no longer be found in the database.
|
|
*/
|
|
function selectedCredsDoNotExist(
|
|
node: INodeUi,
|
|
nodeCredentialType: string,
|
|
storedCredsByType: ICredentialsResponse[] | null,
|
|
) {
|
|
if (!node.credentials || !storedCredsByType) return false;
|
|
|
|
const selectedCredsByType = node.credentials[nodeCredentialType];
|
|
|
|
if (!selectedCredsByType) return false;
|
|
|
|
return !storedCredsByType.find((c) => c.id === selectedCredsByType.id);
|
|
}
|
|
|
|
function updateNodesCredentialsIssues() {
|
|
const nodes = workflowsStore.allNodes;
|
|
let issues: INodeIssues | null;
|
|
|
|
for (const node of nodes) {
|
|
issues = getNodeCredentialIssues(node);
|
|
|
|
workflowsStore.setNodeIssue({
|
|
node: node.name,
|
|
type: 'credentials',
|
|
value: issues?.credentials ?? null,
|
|
});
|
|
}
|
|
}
|
|
|
|
function getNodeTaskData(node: INodeUi | null, runIndex = 0) {
|
|
if (node === null) {
|
|
return null;
|
|
}
|
|
if (workflowsStore.getWorkflowExecution === null) {
|
|
return null;
|
|
}
|
|
|
|
const executionData = workflowsStore.getWorkflowExecution.data;
|
|
if (!executionData?.resultData) {
|
|
// unknown status
|
|
return null;
|
|
}
|
|
const runData = executionData.resultData.runData;
|
|
|
|
const taskData = get(runData, [node.name, runIndex]);
|
|
if (!taskData) {
|
|
return null;
|
|
}
|
|
|
|
return taskData;
|
|
}
|
|
|
|
function getNodeInputData(
|
|
node: INodeUi | null,
|
|
runIndex = 0,
|
|
outputIndex = 0,
|
|
paneType: NodePanelType = 'output',
|
|
connectionType: NodeConnectionType = NodeConnectionType.Main,
|
|
): INodeExecutionData[] {
|
|
//TODO: check if this needs to be fixed in different place
|
|
if (
|
|
node?.type === SPLIT_IN_BATCHES_NODE_TYPE &&
|
|
paneType === 'input' &&
|
|
runIndex !== 0 &&
|
|
outputIndex !== 0
|
|
) {
|
|
runIndex = runIndex - 1;
|
|
}
|
|
|
|
const taskData = getNodeTaskData(node, runIndex);
|
|
if (taskData === null) {
|
|
return [];
|
|
}
|
|
|
|
let data: ITaskDataConnections | undefined = taskData.data;
|
|
if (paneType === 'input' && taskData.inputOverride) {
|
|
data = taskData.inputOverride;
|
|
}
|
|
|
|
if (!data) {
|
|
return [];
|
|
}
|
|
|
|
return getInputData(data, outputIndex, connectionType);
|
|
}
|
|
|
|
function getInputData(
|
|
connectionsData: ITaskDataConnections,
|
|
outputIndex: number,
|
|
connectionType: NodeConnectionType = NodeConnectionType.Main,
|
|
): INodeExecutionData[] {
|
|
return connectionsData?.[connectionType]?.[outputIndex] ?? [];
|
|
}
|
|
|
|
function getBinaryData(
|
|
workflowRunData: IRunData | null,
|
|
node: string | null,
|
|
runIndex: number,
|
|
outputIndex: number,
|
|
connectionType: NodeConnectionType = NodeConnectionType.Main,
|
|
): IBinaryKeyData[] {
|
|
if (node === null) {
|
|
return [];
|
|
}
|
|
|
|
const runData: IRunData | null = workflowRunData;
|
|
|
|
const runDataOfNode = runData?.[node]?.[runIndex]?.data;
|
|
if (!runDataOfNode) {
|
|
return [];
|
|
}
|
|
|
|
const inputData = getInputData(runDataOfNode, outputIndex, connectionType);
|
|
|
|
const returnData: IBinaryKeyData[] = [];
|
|
for (let i = 0; i < inputData.length; i++) {
|
|
const binaryDataInIdx = inputData[i]?.binary;
|
|
if (binaryDataInIdx !== undefined) {
|
|
returnData.push(binaryDataInIdx);
|
|
}
|
|
}
|
|
|
|
return returnData;
|
|
}
|
|
|
|
function disableNodes(nodes: INodeUi[], { trackHistory = false, trackBulk = true } = {}) {
|
|
const telemetry = useTelemetry();
|
|
|
|
if (trackHistory && trackBulk) {
|
|
historyStore.startRecordingUndo();
|
|
}
|
|
|
|
const newDisabledState = nodes.some((node) => !node.disabled);
|
|
for (const node of nodes) {
|
|
if (newDisabledState === node.disabled) {
|
|
continue;
|
|
}
|
|
|
|
// Toggle disabled flag
|
|
const updateInformation = {
|
|
name: node.name,
|
|
properties: {
|
|
disabled: newDisabledState,
|
|
} as IDataObject,
|
|
} as INodeUpdatePropertiesInformation;
|
|
|
|
telemetry.track('User set node enabled status', {
|
|
node_type: node.type,
|
|
is_enabled: node.disabled,
|
|
workflow_id: workflowsStore.workflowId,
|
|
});
|
|
|
|
workflowsStore.updateNodeProperties(updateInformation);
|
|
workflowsStore.clearNodeExecutionData(node.name);
|
|
updateNodeParameterIssues(node);
|
|
updateNodeCredentialIssues(node);
|
|
updateNodesInputIssues();
|
|
if (trackHistory) {
|
|
historyStore.pushCommandToUndo(
|
|
new EnableNodeToggleCommand(node.name, node.disabled === true, newDisabledState),
|
|
);
|
|
}
|
|
}
|
|
|
|
if (trackHistory && trackBulk) {
|
|
historyStore.stopRecordingUndo();
|
|
}
|
|
}
|
|
|
|
function getNodeSubtitle(
|
|
data: INode,
|
|
nodeType: INodeTypeDescription,
|
|
workflow: Workflow,
|
|
): string | undefined {
|
|
if (!data) {
|
|
return undefined;
|
|
}
|
|
|
|
if (data.notesInFlow) {
|
|
return data.notes;
|
|
}
|
|
|
|
if (nodeType?.subtitle !== undefined) {
|
|
try {
|
|
ExpressionEvaluatorProxy.setEvaluator(
|
|
useSettingsStore().settings.expressions?.evaluator ?? 'tmpl',
|
|
);
|
|
return workflow.expression.getSimpleParameterValue(
|
|
data,
|
|
nodeType.subtitle,
|
|
'internal',
|
|
{},
|
|
undefined,
|
|
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
|
) as string | undefined;
|
|
} catch (e) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
if (data.parameters.operation !== undefined) {
|
|
const operation = data.parameters.operation as string;
|
|
if (nodeType === null) {
|
|
return operation;
|
|
}
|
|
|
|
const operationData = nodeType.properties.find((property: INodeProperties) => {
|
|
return property.name === 'operation';
|
|
});
|
|
if (operationData === undefined) {
|
|
return operation;
|
|
}
|
|
|
|
if (operationData.options === undefined) {
|
|
return operation;
|
|
}
|
|
|
|
const optionData = operationData.options.find((option) => {
|
|
return (option as INodePropertyOptions).value === data.parameters.operation;
|
|
});
|
|
if (optionData === undefined) {
|
|
return operation;
|
|
}
|
|
|
|
return optionData.name;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function setSuccessOutput(data: ITaskData[], sourceNode: INodeUi | null) {
|
|
if (!sourceNode) {
|
|
throw new Error('Source node is null or not defined');
|
|
}
|
|
|
|
const allNodeConnections = workflowsStore.outgoingConnectionsByNodeName(sourceNode.name);
|
|
|
|
const connectionType = Object.keys(allNodeConnections)[0] as NodeConnectionType;
|
|
const nodeConnections = allNodeConnections[connectionType];
|
|
const outputMap = NodeViewUtils.getOutputSummary(
|
|
data,
|
|
nodeConnections || [],
|
|
connectionType ?? NodeConnectionType.Main,
|
|
);
|
|
const sourceNodeType = nodeTypesStore.getNodeType(sourceNode.type, sourceNode.typeVersion);
|
|
|
|
Object.keys(outputMap).forEach((sourceOutputIndex: string) => {
|
|
Object.keys(outputMap[sourceOutputIndex]).forEach((targetNodeName: string) => {
|
|
Object.keys(outputMap[sourceOutputIndex][targetNodeName]).forEach(
|
|
(targetInputIndex: string) => {
|
|
if (targetNodeName) {
|
|
const targetNode = workflowsStore.getNodeByName(targetNodeName);
|
|
const connection = NodeViewUtils.getJSPlumbConnection(
|
|
sourceNode,
|
|
parseInt(sourceOutputIndex, 10),
|
|
targetNode,
|
|
parseInt(targetInputIndex, 10),
|
|
connectionType,
|
|
sourceNodeType,
|
|
canvasStore.jsPlumbInstance,
|
|
);
|
|
|
|
if (connection) {
|
|
const output = outputMap[sourceOutputIndex][targetNodeName][targetInputIndex];
|
|
|
|
if (output.isArtificialRecoveredEventItem) {
|
|
NodeViewUtils.recoveredConnection(connection);
|
|
} else if (!output?.total && !output.isArtificialRecoveredEventItem) {
|
|
NodeViewUtils.resetConnection(connection);
|
|
} else {
|
|
NodeViewUtils.addConnectionOutputSuccess(connection, output);
|
|
}
|
|
}
|
|
}
|
|
|
|
const endpoint = NodeViewUtils.getPlusEndpoint(
|
|
sourceNode,
|
|
parseInt(sourceOutputIndex, 10),
|
|
canvasStore.jsPlumbInstance,
|
|
);
|
|
if (endpoint?.endpoint) {
|
|
const output = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0];
|
|
|
|
if (output && output.total > 0) {
|
|
(endpoint.endpoint as N8nPlusEndpoint).setSuccessOutput(
|
|
NodeViewUtils.getRunItemsLabel(output),
|
|
);
|
|
} else {
|
|
(endpoint.endpoint as N8nPlusEndpoint).clearSuccessOutput();
|
|
}
|
|
}
|
|
},
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
function matchCredentials(node: INodeUi) {
|
|
if (!node.credentials) {
|
|
return;
|
|
}
|
|
Object.entries(node.credentials).forEach(
|
|
([nodeCredentialType, nodeCredentials]: [string, INodeCredentialsDetails]) => {
|
|
const credentialOptions = credentialsStore.getCredentialsByType(nodeCredentialType);
|
|
|
|
// Check if workflows applies old credentials style
|
|
if (typeof nodeCredentials === 'string') {
|
|
nodeCredentials = {
|
|
id: null,
|
|
name: nodeCredentials,
|
|
};
|
|
credentialsUpdated.value = true;
|
|
}
|
|
|
|
if (nodeCredentials.id) {
|
|
// Check whether the id is matching with a credential
|
|
const credentialsId = nodeCredentials.id.toString(); // due to a fixed bug in the migration UpdateWorkflowCredentials (just sqlite) we have to cast to string and check later if it has been a number
|
|
const credentialsForId = credentialOptions.find(
|
|
(optionData: ICredentialsResponse) => optionData.id === credentialsId,
|
|
);
|
|
if (credentialsForId) {
|
|
if (
|
|
credentialsForId.name !== nodeCredentials.name ||
|
|
typeof nodeCredentials.id === 'number'
|
|
) {
|
|
node.credentials![nodeCredentialType] = {
|
|
id: credentialsForId.id,
|
|
name: credentialsForId.name,
|
|
};
|
|
credentialsUpdated.value = true;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// No match for id found or old credentials type used
|
|
node.credentials![nodeCredentialType] = nodeCredentials;
|
|
|
|
// check if only one option with the name would exist
|
|
const credentialsForName = credentialOptions.filter(
|
|
(optionData: ICredentialsResponse) => optionData.name === nodeCredentials.name,
|
|
);
|
|
|
|
// only one option exists for the name, take it
|
|
if (credentialsForName.length === 1) {
|
|
node.credentials![nodeCredentialType].id = credentialsForName[0].id;
|
|
credentialsUpdated.value = true;
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
function deleteJSPlumbConnection(connection: Connection, trackHistory = false) {
|
|
// Make sure to remove the overlay else after the second move
|
|
// it visibly stays behind free floating without a connection.
|
|
connection.removeOverlays();
|
|
|
|
pullConnActiveNodeName.value = null; // prevent new connections when connectionDetached is triggered
|
|
canvasStore.jsPlumbInstance?.deleteConnection(connection); // on delete, triggers connectionDetached event which applies mutation to store
|
|
if (trackHistory && connection.__meta) {
|
|
const connectionData: [IConnection, IConnection] = [
|
|
{
|
|
index: connection.__meta?.sourceOutputIndex,
|
|
node: connection.__meta.sourceNodeName,
|
|
type: NodeConnectionType.Main,
|
|
},
|
|
{
|
|
index: connection.__meta?.targetOutputIndex,
|
|
node: connection.__meta.targetNodeName,
|
|
type: NodeConnectionType.Main,
|
|
},
|
|
];
|
|
const removeCommand = new RemoveConnectionCommand(connectionData);
|
|
historyStore.pushCommandToUndo(removeCommand);
|
|
}
|
|
}
|
|
|
|
async function loadNodesProperties(nodeInfos: INodeTypeNameVersion[]): Promise<void> {
|
|
const allNodes: INodeTypeDescription[] = nodeTypesStore.allNodeTypes;
|
|
|
|
const nodesToBeFetched: INodeTypeNameVersion[] = [];
|
|
allNodes.forEach((node) => {
|
|
const nodeVersions = Array.isArray(node.version) ? node.version : [node.version];
|
|
if (
|
|
!!nodeInfos.find((n) => n.name === node.name && nodeVersions.includes(n.version)) &&
|
|
!node.hasOwnProperty('properties')
|
|
) {
|
|
nodesToBeFetched.push({
|
|
name: node.name,
|
|
version: Array.isArray(node.version) ? node.version.slice(-1)[0] : node.version,
|
|
});
|
|
}
|
|
});
|
|
|
|
if (nodesToBeFetched.length > 0) {
|
|
// Only call API if node information is actually missing
|
|
canvasStore.startLoading();
|
|
await nodeTypesStore.getNodesInformation(nodesToBeFetched);
|
|
canvasStore.stopLoading();
|
|
}
|
|
}
|
|
|
|
function addConnectionsTestData() {
|
|
canvasStore.jsPlumbInstance?.connections.forEach((connection) => {
|
|
NodeViewUtils.addConnectionTestData(
|
|
connection.source,
|
|
connection.target,
|
|
connection?.connector?.hasOwnProperty('canvas') ? connection?.connector.canvas : undefined,
|
|
);
|
|
});
|
|
}
|
|
|
|
async function processConnectionBatch(batchedConnectionData: Array<[IConnection, IConnection]>) {
|
|
const batchSize = 100;
|
|
|
|
for (let i = 0; i < batchedConnectionData.length; i += batchSize) {
|
|
const batch = batchedConnectionData.slice(i, i + batchSize);
|
|
|
|
batch.forEach((connectionData) => {
|
|
addConnection(connectionData);
|
|
});
|
|
}
|
|
}
|
|
|
|
function addPinDataConnections(pinData?: IPinData) {
|
|
if (!pinData) {
|
|
return;
|
|
}
|
|
|
|
Object.keys(pinData).forEach((nodeName) => {
|
|
const node = workflowsStore.getNodeByName(nodeName);
|
|
if (!node) {
|
|
return;
|
|
}
|
|
|
|
const nodeElement = document.getElementById(node.id);
|
|
if (!nodeElement) {
|
|
return;
|
|
}
|
|
|
|
const hasRun = workflowsStore.getWorkflowResultDataByNodeName(nodeName) !== null;
|
|
// In case we are showing a production execution preview we want
|
|
// to show pinned data connections as they wouldn't have been pinned
|
|
const classNames = isProductionExecutionPreview.value ? [] : ['pinned'];
|
|
|
|
if (hasRun) {
|
|
classNames.push('has-run');
|
|
}
|
|
|
|
const connections = canvasStore.jsPlumbInstance?.getConnections({
|
|
source: nodeElement,
|
|
});
|
|
|
|
const connectionsArray = Array.isArray(connections)
|
|
? connections
|
|
: Object.values(connections);
|
|
|
|
connectionsArray.forEach((connection) => {
|
|
NodeViewUtils.addConnectionOutputSuccess(connection, {
|
|
total: pinData[nodeName].length,
|
|
iterations: 0,
|
|
classNames,
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function removePinDataConnections(event: UnpinNodeDataEvent) {
|
|
for (const nodeName of event.nodeNames) {
|
|
const node = workflowsStore.getNodeByName(nodeName);
|
|
if (!node) {
|
|
return;
|
|
}
|
|
|
|
const nodeElement = document.getElementById(node.id);
|
|
if (!nodeElement) {
|
|
return;
|
|
}
|
|
|
|
const connections = canvasStore.jsPlumbInstance?.getConnections({
|
|
source: nodeElement,
|
|
});
|
|
|
|
const connectionsArray = Array.isArray(connections)
|
|
? connections
|
|
: Object.values(connections);
|
|
|
|
canvasStore.jsPlumbInstance.setSuspendDrawing(true);
|
|
connectionsArray.forEach(NodeViewUtils.resetConnection);
|
|
canvasStore.jsPlumbInstance.setSuspendDrawing(false, true);
|
|
}
|
|
}
|
|
|
|
function getOutputEndpointUUID(
|
|
nodeName: string,
|
|
connectionType: NodeConnectionType,
|
|
index: number,
|
|
): string | null {
|
|
const node = workflowsStore.getNodeByName(nodeName);
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
|
|
return NodeViewUtils.getOutputEndpointUUID(node.id, connectionType, index);
|
|
}
|
|
function getInputEndpointUUID(
|
|
nodeName: string,
|
|
connectionType: NodeConnectionType,
|
|
index: number,
|
|
) {
|
|
const node = workflowsStore.getNodeByName(nodeName);
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
|
|
return NodeViewUtils.getInputEndpointUUID(node.id, connectionType, index);
|
|
}
|
|
|
|
function addConnection(connection: [IConnection, IConnection]) {
|
|
const outputUuid = getOutputEndpointUUID(
|
|
connection[0].node,
|
|
connection[0].type,
|
|
connection[0].index,
|
|
);
|
|
const inputUuid = getInputEndpointUUID(
|
|
connection[1].node,
|
|
connection[1].type,
|
|
connection[1].index,
|
|
);
|
|
if (!outputUuid || !inputUuid) {
|
|
return;
|
|
}
|
|
|
|
const uuids: [string, string] = [outputUuid, inputUuid];
|
|
// Create connections in DOM
|
|
canvasStore.jsPlumbInstance?.connect({
|
|
uuids,
|
|
detachable: !route?.meta?.readOnlyCanvas && !sourceControlStore.preferences.branchReadOnly,
|
|
});
|
|
|
|
setTimeout(() => {
|
|
addPinDataConnections(workflowsStore.pinnedWorkflowData);
|
|
});
|
|
}
|
|
|
|
function removeConnection(
|
|
connection: [IConnection, IConnection],
|
|
removeVisualConnection = false,
|
|
) {
|
|
if (removeVisualConnection) {
|
|
const sourceNode = workflowsStore.getNodeByName(connection[0].node);
|
|
const targetNode = workflowsStore.getNodeByName(connection[1].node);
|
|
|
|
if (!sourceNode || !targetNode) {
|
|
return;
|
|
}
|
|
|
|
const sourceElement = document.getElementById(sourceNode.id);
|
|
const targetElement = document.getElementById(targetNode.id);
|
|
|
|
if (sourceElement && targetElement) {
|
|
const connections = canvasStore.jsPlumbInstance?.getConnections({
|
|
source: sourceElement,
|
|
target: targetElement,
|
|
});
|
|
|
|
if (Array.isArray(connections)) {
|
|
connections.forEach((connectionInstance: Connection) => {
|
|
if (connectionInstance.__meta) {
|
|
// Only delete connections from specific indexes (if it can be determined by meta)
|
|
if (
|
|
connectionInstance.__meta.sourceOutputIndex === connection[0].index &&
|
|
connectionInstance.__meta.targetOutputIndex === connection[1].index
|
|
) {
|
|
deleteJSPlumbConnection(connectionInstance);
|
|
}
|
|
} else {
|
|
deleteJSPlumbConnection(connectionInstance);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
workflowsStore.removeConnection({ connection });
|
|
}
|
|
|
|
function removeConnectionByConnectionInfo(
|
|
info: ConnectionDetachedParams,
|
|
removeVisualConnection = false,
|
|
trackHistory = false,
|
|
) {
|
|
const connectionInfo: [IConnection, IConnection] | null = getConnectionInfo(info);
|
|
|
|
if (connectionInfo) {
|
|
if (removeVisualConnection) {
|
|
deleteJSPlumbConnection(info.connection, trackHistory);
|
|
} else if (trackHistory) {
|
|
historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo));
|
|
}
|
|
workflowsStore.removeConnection({ connection: connectionInfo });
|
|
}
|
|
}
|
|
|
|
async function addConnections(connections: IConnections) {
|
|
const batchedConnectionData: Array<[IConnection, IConnection]> = [];
|
|
|
|
for (const sourceNode in connections) {
|
|
for (const type in connections[sourceNode]) {
|
|
connections[sourceNode][type].forEach((outwardConnections, sourceIndex) => {
|
|
if (outwardConnections) {
|
|
outwardConnections.forEach((targetData) => {
|
|
batchedConnectionData.push([
|
|
{
|
|
node: sourceNode,
|
|
type: getEndpointScope(type) ?? NodeConnectionType.Main,
|
|
index: sourceIndex,
|
|
},
|
|
{ node: targetData.node, type: targetData.type, index: targetData.index },
|
|
]);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Process the connections in batches
|
|
await processConnectionBatch(batchedConnectionData);
|
|
setTimeout(addConnectionsTestData, 0);
|
|
}
|
|
|
|
async function addNodes(nodes: INodeUi[], connections?: IConnections, trackHistory = false) {
|
|
if (!nodes?.length) {
|
|
return;
|
|
}
|
|
isInsertingNodes.value = true;
|
|
// Before proceeding we must check if all nodes contain the `properties` attribute.
|
|
// Nodes are loaded without this information so we must make sure that all nodes
|
|
// being added have this information.
|
|
await loadNodesProperties(
|
|
nodes.map((node) => ({ name: node.type, version: node.typeVersion })),
|
|
);
|
|
|
|
// Add the node to the node-list
|
|
let nodeType: INodeTypeDescription | null;
|
|
nodes.forEach((node) => {
|
|
const newNode: INodeUi = {
|
|
...node,
|
|
};
|
|
|
|
if (!newNode.id) {
|
|
assignNodeId(newNode);
|
|
}
|
|
|
|
nodeType = nodeTypesStore.getNodeType(newNode.type, newNode.typeVersion);
|
|
|
|
// Make sure that some properties always exist
|
|
if (!newNode.hasOwnProperty('disabled')) {
|
|
newNode.disabled = false;
|
|
}
|
|
|
|
if (!newNode.hasOwnProperty('parameters')) {
|
|
newNode.parameters = {};
|
|
}
|
|
|
|
// Load the default parameter values because only values which differ
|
|
// from the defaults get saved
|
|
if (nodeType !== null) {
|
|
let nodeParameters = null;
|
|
try {
|
|
nodeParameters = NodeHelpers.getNodeParameters(
|
|
nodeType.properties,
|
|
newNode.parameters,
|
|
true,
|
|
false,
|
|
node,
|
|
);
|
|
} catch (e) {
|
|
console.error(
|
|
i18n.baseText('nodeView.thereWasAProblemLoadingTheNodeParametersOfNode') +
|
|
`: "${newNode.name}"`,
|
|
);
|
|
console.error(e);
|
|
}
|
|
newNode.parameters = nodeParameters ?? {};
|
|
|
|
// if it's a webhook and the path is empty set the UUID as the default path
|
|
if (
|
|
[WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE].includes(newNode.type) &&
|
|
newNode.parameters.path === ''
|
|
) {
|
|
newNode.parameters.path = newNode.webhookId as string;
|
|
}
|
|
}
|
|
|
|
// check and match credentials, apply new format if old is used
|
|
matchCredentials(newNode);
|
|
workflowsStore.addNode(newNode);
|
|
if (trackHistory) {
|
|
historyStore.pushCommandToUndo(new AddNodeCommand(newNode));
|
|
}
|
|
});
|
|
|
|
// Wait for the nodes to be rendered
|
|
await nextTick();
|
|
|
|
canvasStore.jsPlumbInstance?.setSuspendDrawing(true);
|
|
|
|
if (connections) {
|
|
await addConnections(connections);
|
|
}
|
|
// Add the node issues at the end as the node-connections are required
|
|
refreshNodeIssues();
|
|
updateNodesInputIssues();
|
|
/////////////////////////////this.resetEndpointsErrors();
|
|
isInsertingNodes.value = false;
|
|
|
|
// Now it can draw again
|
|
canvasStore.jsPlumbInstance?.setSuspendDrawing(false, true);
|
|
}
|
|
|
|
function assignNodeId(node: INodeUi) {
|
|
const id = window.crypto.randomUUID();
|
|
node.id = id;
|
|
return id;
|
|
}
|
|
|
|
function assignWebhookId(node: INodeUi) {
|
|
const id = window.crypto.randomUUID();
|
|
node.webhookId = id;
|
|
return id;
|
|
}
|
|
|
|
return {
|
|
hasProxyAuth,
|
|
isCustomApiCallSelected,
|
|
getParameterValue,
|
|
displayParameter,
|
|
getNodeIssues,
|
|
updateNodesInputIssues,
|
|
updateNodesExecutionIssues,
|
|
updateNodesParameterIssues,
|
|
updateNodeInputIssues,
|
|
updateNodeCredentialIssuesByName,
|
|
updateNodeCredentialIssues,
|
|
updateNodeParameterIssuesByName,
|
|
updateNodeParameterIssues,
|
|
getBinaryData,
|
|
disableNodes,
|
|
getNodeSubtitle,
|
|
updateNodesCredentialsIssues,
|
|
getNodeInputData,
|
|
setSuccessOutput,
|
|
matchCredentials,
|
|
isInsertingNodes,
|
|
credentialsUpdated,
|
|
isProductionExecutionPreview,
|
|
pullConnActiveNodeName,
|
|
deleteJSPlumbConnection,
|
|
loadNodesProperties,
|
|
addNodes,
|
|
addConnections,
|
|
addConnection,
|
|
removeConnection,
|
|
removeConnectionByConnectionInfo,
|
|
addPinDataConnections,
|
|
removePinDataConnections,
|
|
getNodeTaskData,
|
|
assignNodeId,
|
|
assignWebhookId,
|
|
};
|
|
}
|