feat(core): Add support for building LLM applications (#7235)

This extracts all core and editor changes from #7246 and #7137, so that
we can get these changes merged first.

ADO-1120

[DB Tests](https://github.com/n8n-io/n8n/actions/runs/6379749011)
[E2E Tests](https://github.com/n8n-io/n8n/actions/runs/6379751480)
[Workflow Tests](https://github.com/n8n-io/n8n/actions/runs/6379752828)

---------

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
Co-authored-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Alex Grozav <alex@grozav.com>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2023-10-02 17:33:43 +02:00
committed by GitHub
parent 04dfcd73be
commit 00a4b8b0c6
93 changed files with 6209 additions and 728 deletions

View File

@@ -4,9 +4,20 @@ import { mapStores } from 'pinia';
import type { INodeUi } from '@/Interface';
import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers';
import { NO_OP_NODE_TYPE } from '@/constants';
import {
NO_OP_NODE_TYPE,
NODE_CONNECTION_TYPE_ALLOW_MULTIPLE,
NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS,
NODE_MIN_INPUT_ITEMS_COUNT,
} from '@/constants';
import type { INodeTypeDescription } from 'n8n-workflow';
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow';
import type {
ConnectionTypes,
INodeInputConfiguration,
INodeTypeDescription,
INodeOutputConfiguration,
} from 'n8n-workflow';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
@@ -15,6 +26,33 @@ import type { Endpoint, EndpointOptions } from '@jsplumb/core';
import * as NodeViewUtils from '@/utils/nodeViewUtils';
import { useHistoryStore } from '@/stores/history.store';
import { useCanvasStore } from '@/stores/canvas.store';
import type { EndpointSpec } from '@jsplumb/common';
const createAddInputEndpointSpec = (
connectionName: NodeConnectionType,
color: string,
): EndpointSpec => {
const multiple = NODE_CONNECTION_TYPE_ALLOW_MULTIPLE.includes(connectionName);
return {
type: 'N8nAddInput',
options: {
width: 24,
height: 72,
color,
multiple,
},
};
};
const createDiamondOutputEndpointSpec = (): EndpointSpec => ({
type: 'Rectangle',
options: {
height: 10,
width: 10,
cssClass: 'diamond-output-endpoint',
},
});
export const nodeBase = defineComponent({
mixins: [deviceSupportHelpers],
@@ -29,6 +67,12 @@ export const nodeBase = defineComponent({
}
}
},
data() {
return {
inputs: [] as Array<ConnectionTypes | INodeInputConfiguration>,
outputs: [] as Array<ConnectionTypes | INodeOutputConfiguration>,
};
},
computed: {
...mapStores(useNodeTypesStore, useUIStore, useCanvasStore, useWorkflowsStore, useHistoryStore),
data(): INodeUi | null {
@@ -72,59 +116,151 @@ export const nodeBase = defineComponent({
},
__addInputEndpoints(node: INodeUi, nodeTypeData: INodeTypeDescription) {
// Add Inputs
let index;
const indexData: {
const rootTypeIndexData: {
[key: string]: number;
} = {};
const typeIndexData: {
[key: string]: number;
} = {};
nodeTypeData.inputs.forEach((inputName: string, i: number) => {
// Increment the index for inputs with current name
if (indexData.hasOwnProperty(inputName)) {
indexData[inputName]++;
} else {
indexData[inputName] = 0;
const workflow = this.workflowsStore.getCurrentWorkflow();
const inputs: Array<ConnectionTypes | INodeInputConfiguration> =
NodeHelpers.getNodeInputs(workflow, this.data!, nodeTypeData) || [];
this.inputs = inputs;
const sortedInputs = [...inputs];
sortedInputs.sort((a, b) => {
if (typeof a === 'string') {
return 1;
} else if (typeof b === 'string') {
return -1;
}
index = indexData[inputName];
if (a.required && !b.required) {
return -1;
} else if (!a.required && b.required) {
return 1;
}
return 0;
});
sortedInputs.forEach((value, i) => {
let inputConfiguration: INodeInputConfiguration;
if (typeof value === 'string') {
inputConfiguration = {
type: value,
};
} else {
inputConfiguration = value;
}
const inputName: ConnectionTypes = inputConfiguration.type;
const rootCategoryInputName =
inputName === NodeConnectionType.Main ? NodeConnectionType.Main : 'other';
// Increment the index for inputs with current name
if (rootTypeIndexData.hasOwnProperty(rootCategoryInputName)) {
rootTypeIndexData[rootCategoryInputName]++;
} else {
rootTypeIndexData[rootCategoryInputName] = 0;
}
if (typeIndexData.hasOwnProperty(inputName)) {
typeIndexData[inputName]++;
} else {
typeIndexData[inputName] = 0;
}
const rootTypeIndex = rootTypeIndexData[rootCategoryInputName];
const typeIndex = typeIndexData[inputName];
const inputsOfSameRootType = inputs.filter((inputData) => {
const thisInputName: string = typeof inputData === 'string' ? inputData : inputData.type;
return inputName === NodeConnectionType.Main
? thisInputName === NodeConnectionType.Main
: thisInputName !== NodeConnectionType.Main;
});
const nonMainInputs = inputsOfSameRootType.filter((inputData) => {
return inputData !== NodeConnectionType.Main;
});
const requiredNonMainInputs = nonMainInputs.filter((inputData) => {
return typeof inputData !== 'string' && inputData.required;
});
const optionalNonMainInputs = nonMainInputs.filter((inputData) => {
return typeof inputData !== 'string' && !inputData.required;
});
const spacerIndexes = this.getSpacerIndexes(
requiredNonMainInputs.length,
optionalNonMainInputs.length,
);
// Get the position of the anchor depending on how many it has
const anchorPosition =
NodeViewUtils.ANCHOR_POSITIONS.input[nodeTypeData.inputs.length][index];
const anchorPosition = NodeViewUtils.getAnchorPosition(
inputName,
'input',
inputsOfSameRootType.length,
spacerIndexes,
)[rootTypeIndex];
const scope = NodeViewUtils.getEndpointScope(inputName as NodeConnectionType);
const newEndpointData: EndpointOptions = {
uuid: NodeViewUtils.getInputEndpointUUID(this.nodeId, index),
uuid: NodeViewUtils.getInputEndpointUUID(this.nodeId, inputName, typeIndex),
anchor: anchorPosition,
maxConnections: -1,
// We potentially want to change that in the future to allow people to dynamically
// activate and deactivate connected nodes
maxConnections: inputConfiguration.maxConnections ?? -1,
endpoint: 'Rectangle',
paintStyle: NodeViewUtils.getInputEndpointStyle(nodeTypeData, '--color-foreground-xdark'),
hoverPaintStyle: NodeViewUtils.getInputEndpointStyle(nodeTypeData, '--color-primary'),
source: false,
target: !this.isReadOnly && nodeTypeData.inputs.length > 1, // only enabled for nodes with multiple inputs.. otherwise attachment handled by connectionDrag event in NodeView,
paintStyle: NodeViewUtils.getInputEndpointStyle(
nodeTypeData,
'--color-foreground-xdark',
inputName,
),
hoverPaintStyle: NodeViewUtils.getInputEndpointStyle(
nodeTypeData,
'--color-primary',
inputName,
),
scope: NodeViewUtils.getScope(scope),
source: inputName !== NodeConnectionType.Main,
target: !this.isReadOnly && inputs.length > 1, // only enabled for nodes with multiple inputs.. otherwise attachment handled by connectionDrag event in NodeView,
parameters: {
connection: 'target',
nodeId: this.nodeId,
type: inputName,
index,
index: typeIndex,
},
enabled: !this.isReadOnly, // enabled in default case to allow dragging
cssClass: 'rect-input-endpoint',
dragAllowedWhenFull: true,
hoverClass: 'dropHover',
hoverClass: 'rect-input-endpoint-hover',
...this.__getInputConnectionStyle(inputName, nodeTypeData),
};
const endpoint = this.instance?.addEndpoint(
this.$refs[this.data.name] as Element,
newEndpointData,
);
this.__addEndpointTestingData(endpoint, 'input', index);
if (nodeTypeData.inputNames) {
) as Endpoint;
this.__addEndpointTestingData(endpoint, 'input', typeIndex);
if (inputConfiguration.displayName || nodeTypeData.inputNames?.[i]) {
// Apply input names if they got set
endpoint.addOverlay(NodeViewUtils.getInputNameOverlay(nodeTypeData.inputNames[index]));
endpoint.addOverlay(
NodeViewUtils.getInputNameOverlay(
inputConfiguration.displayName || nodeTypeData.inputNames[i],
inputName,
inputConfiguration.required,
),
);
}
if (!Array.isArray(endpoint)) {
endpoint.__meta = {
nodeName: node.name,
nodeId: this.nodeId,
index: i,
totalEndpoints: nodeTypeData.inputs.length,
index: typeIndex,
totalEndpoints: inputsOfSameRootType.length,
};
}
@@ -134,71 +270,166 @@ export const nodeBase = defineComponent({
// different to the regular one (have different ids). So that seems to make
// problems when hiding the input-name.
// if (index === 0 && inputName === 'main') {
// if (index === 0 && inputName === NodeConnectionType.Main) {
// // Make the first main-input the default one to connect to when connection gets dropped on node
// this.instance.makeTarget(this.nodeId, newEndpointData);
// }
});
if (nodeTypeData.inputs.length === 0) {
if (sortedInputs.length === 0) {
this.instance.manage(this.$refs[this.data.name] as Element);
}
},
getSpacerIndexes(
leftGroupItemsCount: number,
rightGroupItemsCount: number,
insertSpacerBetweenGroups = NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS,
minItemsCount = NODE_MIN_INPUT_ITEMS_COUNT,
): number[] {
const spacerIndexes = [];
if (leftGroupItemsCount > 0 && rightGroupItemsCount > 0) {
if (insertSpacerBetweenGroups) {
spacerIndexes.push(leftGroupItemsCount);
} else if (leftGroupItemsCount + rightGroupItemsCount < minItemsCount) {
for (
let spacerIndex = leftGroupItemsCount;
spacerIndex < minItemsCount - rightGroupItemsCount;
spacerIndex++
) {
spacerIndexes.push(spacerIndex);
}
}
} else {
if (
leftGroupItemsCount > 0 &&
leftGroupItemsCount < minItemsCount &&
rightGroupItemsCount === 0
) {
for (
let spacerIndex = 0;
spacerIndex < minItemsCount - leftGroupItemsCount;
spacerIndex++
) {
spacerIndexes.push(spacerIndex + leftGroupItemsCount);
}
} else if (
leftGroupItemsCount === 0 &&
rightGroupItemsCount > 0 &&
rightGroupItemsCount < minItemsCount
) {
for (
let spacerIndex = 0;
spacerIndex < minItemsCount - rightGroupItemsCount;
spacerIndex++
) {
spacerIndexes.push(spacerIndex);
}
}
}
return spacerIndexes;
},
__addOutputEndpoints(node: INodeUi, nodeTypeData: INodeTypeDescription) {
let index;
const indexData: {
const rootTypeIndexData: {
[key: string]: number;
} = {};
const typeIndexData: {
[key: string]: number;
} = {};
nodeTypeData.outputs.forEach((inputName: string, i: number) => {
// Increment the index for outputs with current name
if (indexData.hasOwnProperty(inputName)) {
indexData[inputName]++;
const workflow = this.workflowsStore.getCurrentWorkflow();
const outputs = NodeHelpers.getNodeOutputs(workflow, this.data, nodeTypeData) || [];
this.outputs = outputs;
// TODO: There are still a lot of references of "main" in NodesView and
// other locations. So assume there will be more problems
outputs.forEach((value, i) => {
let outputConfiguration: INodeOutputConfiguration;
if (typeof value === 'string') {
outputConfiguration = {
type: value,
};
} else {
indexData[inputName] = 0;
outputConfiguration = value;
}
index = indexData[inputName];
const outputName: ConnectionTypes = outputConfiguration.type;
const rootCategoryOutputName =
outputName === NodeConnectionType.Main ? NodeConnectionType.Main : 'other';
// Increment the index for outputs with current name
if (rootTypeIndexData.hasOwnProperty(rootCategoryOutputName)) {
rootTypeIndexData[rootCategoryOutputName]++;
} else {
rootTypeIndexData[rootCategoryOutputName] = 0;
}
if (typeIndexData.hasOwnProperty(outputName)) {
typeIndexData[outputName]++;
} else {
typeIndexData[outputName] = 0;
}
const rootTypeIndex = rootTypeIndexData[rootCategoryOutputName];
const typeIndex = typeIndexData[outputName];
const outputsOfSameRootType = outputs.filter((outputData) => {
const thisOutputName: string =
typeof outputData === 'string' ? outputData : outputData.type;
return outputName === NodeConnectionType.Main
? thisOutputName === NodeConnectionType.Main
: thisOutputName !== NodeConnectionType.Main;
});
// Get the position of the anchor depending on how many it has
const anchorPosition =
NodeViewUtils.ANCHOR_POSITIONS.output[nodeTypeData.outputs.length][index];
const anchorPosition = NodeViewUtils.getAnchorPosition(
outputName,
'output',
outputsOfSameRootType.length,
)[rootTypeIndex];
const scope = NodeViewUtils.getEndpointScope(outputName as NodeConnectionType);
const newEndpointData: EndpointOptions = {
uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, index),
uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, outputName, typeIndex),
anchor: anchorPosition,
maxConnections: -1,
endpoint: {
type: 'Dot',
options: {
radius: nodeTypeData && nodeTypeData.outputs.length > 2 ? 7 : 9,
radius: nodeTypeData && outputsOfSameRootType.length > 2 ? 7 : 9,
},
},
paintStyle: NodeViewUtils.getOutputEndpointStyle(
nodeTypeData,
'--color-foreground-xdark',
),
hoverPaintStyle: NodeViewUtils.getOutputEndpointStyle(nodeTypeData, '--color-primary'),
scope,
source: true,
target: false,
target: outputName !== NodeConnectionType.Main,
enabled: !this.isReadOnly,
parameters: {
connection: 'source',
nodeId: this.nodeId,
type: inputName,
index,
type: outputName,
index: typeIndex,
},
hoverClass: 'dot-output-endpoint-hover',
connectionsDirected: true,
cssClass: 'dot-output-endpoint',
dragAllowedWhenFull: false,
...this.__getOutputConnectionStyle(outputName, nodeTypeData),
};
const endpoint = this.instance.addEndpoint(
this.$refs[this.data.name] as Element,
newEndpointData,
);
this.__addEndpointTestingData(endpoint, 'output', index);
if (nodeTypeData.outputNames) {
this.__addEndpointTestingData(endpoint, 'output', typeIndex);
if (outputConfiguration.displayName || nodeTypeData.outputNames?.[i]) {
// Apply output names if they got set
const overlaySpec = NodeViewUtils.getOutputNameOverlay(nodeTypeData.outputNames[index]);
const overlaySpec = NodeViewUtils.getOutputNameOverlay(
outputConfiguration.displayName || nodeTypeData.outputNames[i],
outputName,
);
endpoint.addOverlay(overlaySpec);
}
@@ -206,14 +437,14 @@ export const nodeBase = defineComponent({
endpoint.__meta = {
nodeName: node.name,
nodeId: this.nodeId,
index: i,
totalEndpoints: nodeTypeData.outputs.length,
index: typeIndex,
totalEndpoints: outputsOfSameRootType.length,
};
}
if (!this.isReadOnly) {
if (!this.isReadOnly && outputName === NodeConnectionType.Main) {
const plusEndpointData: EndpointOptions = {
uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, index),
uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, outputName, typeIndex),
anchor: anchorPosition,
maxConnections: -1,
endpoint: {
@@ -221,8 +452,8 @@ export const nodeBase = defineComponent({
options: {
dimensions: 24,
connectedEndpoint: endpoint,
showOutputLabel: nodeTypeData.outputs.length === 1,
size: nodeTypeData.outputs.length >= 3 ? 'small' : 'medium',
showOutputLabel: outputs.length === 1,
size: outputs.length >= 3 ? 'small' : 'medium',
hoverMessage: this.$locale.baseText('nodeBase.clickToAddNodeOrDragToConnect'),
},
},
@@ -236,9 +467,10 @@ export const nodeBase = defineComponent({
outlineStroke: 'none',
},
parameters: {
connection: 'source',
nodeId: this.nodeId,
type: inputName,
index,
type: outputName,
index: typeIndex,
},
cssClass: 'plus-draggable-endpoint',
dragAllowedWhenFull: false,
@@ -247,14 +479,14 @@ export const nodeBase = defineComponent({
this.$refs[this.data.name] as Element,
plusEndpointData,
);
this.__addEndpointTestingData(plusEndpoint, 'plus', index);
this.__addEndpointTestingData(plusEndpoint, 'plus', typeIndex);
if (!Array.isArray(plusEndpoint)) {
plusEndpoint.__meta = {
nodeName: node.name,
nodeId: this.nodeId,
index: i,
totalEndpoints: nodeTypeData.outputs.length,
index: typeIndex,
totalEndpoints: outputsOfSameRootType.length,
};
}
}
@@ -267,6 +499,74 @@ export const nodeBase = defineComponent({
this.__addInputEndpoints(node, nodeTypeData);
this.__addOutputEndpoints(node, nodeTypeData);
},
__getEndpointColor(connectionType: ConnectionTypes) {
return `--node-type-${connectionType}-color`;
},
__getInputConnectionStyle(
connectionType: ConnectionTypes,
nodeTypeData: INodeTypeDescription,
): EndpointOptions {
if (connectionType === NodeConnectionType.Main) {
return {
paintStyle: NodeViewUtils.getInputEndpointStyle(
nodeTypeData,
this.__getEndpointColor(NodeConnectionType.Main),
connectionType,
),
};
}
if (!Object.values(NodeConnectionType).includes(connectionType as NodeConnectionType)) {
return {};
}
const createSupplementalConnectionType = (
connectionName: ConnectionTypes,
): EndpointOptions => ({
endpoint: createAddInputEndpointSpec(
connectionName as NodeConnectionType,
this.__getEndpointColor(connectionName),
),
});
return createSupplementalConnectionType(connectionType);
},
__getOutputConnectionStyle(
connectionType: ConnectionTypes,
nodeTypeData: INodeTypeDescription,
): EndpointOptions {
const type = 'output';
const createSupplementalConnectionType = (
connectionName: ConnectionTypes,
): EndpointOptions => ({
endpoint: createDiamondOutputEndpointSpec(),
paintStyle: NodeViewUtils.getOutputEndpointStyle(
nodeTypeData,
this.__getEndpointColor(connectionName),
),
hoverPaintStyle: NodeViewUtils.getOutputEndpointStyle(
nodeTypeData,
this.__getEndpointColor(connectionName),
),
});
if (connectionType === NodeConnectionType.Main) {
return {
paintStyle: NodeViewUtils.getOutputEndpointStyle(
nodeTypeData,
this.__getEndpointColor(NodeConnectionType.Main),
),
cssClass: `dot-${type}-endpoint`,
};
}
if (!Object.values(NodeConnectionType).includes(connectionType as NodeConnectionType)) {
return {};
}
return createSupplementalConnectionType(connectionType);
},
touchEnd(e: MouseEvent) {
if (this.isTouchDevice) {
if (this.uiStore.isActionActive('dragActive')) {

View File

@@ -3,6 +3,7 @@ import { useHistoryStore } from '@/stores/history.store';
import { PLACEHOLDER_FILLED_AT_EXECUTION_TIME, CUSTOM_API_CALL_KEY } from '@/constants';
import type {
ConnectionTypes,
IBinaryKeyData,
ICredentialType,
INodeCredentialDescription,
@@ -18,14 +19,17 @@ import type {
INode,
INodePropertyOptions,
IDataObject,
Workflow,
INodeInputConfiguration,
} from 'n8n-workflow';
import { NodeHelpers } from 'n8n-workflow';
import { NodeHelpers, ExpressionEvaluatorProxy, NodeConnectionType } from 'n8n-workflow';
import type {
ICredentialsResponse,
INodeUi,
INodeUpdatePropertiesInformation,
IUser,
NodePanelType,
} from '@/Interface';
import { get } from 'lodash-es';
@@ -84,6 +88,24 @@ export const nodeHelpers = defineComponent({
return NodeHelpers.displayParameterPath(nodeValues, parameter, path, node);
},
// Updates all the issues on all the nodes
refreshNodeIssues(): void {
const nodes = this.workflowsStore.allNodes;
let nodeType: INodeTypeDescription | null;
let foundNodeIssues: INodeIssues | null;
nodes.forEach((node) => {
if (node.disabled === true) {
return;
}
nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
foundNodeIssues = this.getNodeIssues(nodeType, node);
if (foundNodeIssues !== null) {
node.issues = foundNodeIssues;
}
});
},
// Returns all the issues of the node
getNodeIssues(
nodeType: INodeTypeDescription | null,
@@ -124,6 +146,14 @@ export const nodeHelpers = defineComponent({
NodeHelpers.mergeIssues(nodeIssues, nodeCredentialIssues);
}
}
const workflow = this.workflowsStore.getCurrentWorkflow();
const nodeInputIssues = this.getNodeInputIssues(workflow, node, nodeType);
if (nodeIssues === null) {
nodeIssues = nodeInputIssues;
} else {
NodeHelpers.mergeIssues(nodeIssues, nodeInputIssues);
}
}
if (this.hasNodeExecutionIssues(node) && !ignoreIssues.includes('execution')) {
@@ -168,6 +198,25 @@ export const nodeHelpers = defineComponent({
};
},
updateNodesInputIssues() {
const nodes = this.workflowsStore.allNodes;
const workflow = this.workflowsStore.getCurrentWorkflow();
for (const node of nodes) {
const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (!nodeType) {
return;
}
const nodeInputIssues = this.getNodeInputIssues(workflow, node, nodeType);
this.workflowsStore.setNodeIssue({
node: node.name,
type: 'input',
value: nodeInputIssues?.input ? nodeInputIssues.input : null,
});
}
},
// Updates the execution issues.
updateNodesExecutionIssues() {
const nodes = this.workflowsStore.allNodes;
@@ -242,6 +291,45 @@ export const nodeHelpers = defineComponent({
});
},
// Returns all the input-issues of the node
getNodeInputIssues(
workflow: Workflow,
node: INodeUi,
nodeType?: INodeTypeDescription,
): INodeIssues | null {
const foundIssues: INodeIssueObjectProperty = {};
const workflowNode = workflow.getNode(node.name);
let inputs: Array<ConnectionTypes | 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] = [
this.$locale.baseText('nodeIssues.input.missing', {
interpolate: { inputName: input.displayName || input.type },
}),
];
}
});
if (Object.keys(foundIssues).length) {
return {
input: foundIssues,
};
}
return null;
},
// Returns all the credential-issues of the node
getNodeCredentialIssues(node: INodeUi, nodeType?: INodeTypeDescription): INodeIssues | null {
if (node.disabled) {
@@ -414,14 +502,20 @@ export const nodeHelpers = defineComponent({
}
},
getNodeInputData(node: INodeUi | null, runIndex = 0, outputIndex = 0): INodeExecutionData[] {
getNodeInputData(
node: INodeUi | null,
runIndex = 0,
outputIndex = 0,
paneType: NodePanelType = 'output',
connectionType: ConnectionTypes = NodeConnectionType.Main,
): INodeExecutionData[] {
if (node === null) {
return [];
}
if (this.workflowsStore.getWorkflowExecution === null) {
return [];
}
const executionData = this.workflowsStore.getWorkflowExecution.data;
if (!executionData?.resultData) {
// unknown status
@@ -429,31 +523,39 @@ export const nodeHelpers = defineComponent({
}
const runData = executionData.resultData.runData;
if (
!runData?.[node.name]?.[runIndex].data ||
runData[node.name][runIndex].data === undefined
) {
const taskData = get(runData, `[${node.name}][${runIndex}]`);
if (!taskData) {
return [];
}
return this.getMainInputData(runData[node.name][runIndex].data!, outputIndex);
let data: ITaskDataConnections | undefined = taskData.data!;
if (paneType === 'input' && taskData.inputOverride) {
data = taskData.inputOverride!;
}
if (!data) {
return [];
}
return this.getInputData(data, outputIndex, connectionType);
},
// Returns the data of the main input
getMainInputData(
getInputData(
connectionsData: ITaskDataConnections,
outputIndex: number,
connectionType: ConnectionTypes = NodeConnectionType.Main,
): INodeExecutionData[] {
if (
!connectionsData ||
!connectionsData.hasOwnProperty('main') ||
connectionsData.main === undefined ||
connectionsData.main.length < outputIndex ||
connectionsData.main[outputIndex] === null
!connectionsData.hasOwnProperty(connectionType) ||
connectionsData[connectionType] === undefined ||
connectionsData[connectionType].length < outputIndex ||
connectionsData[connectionType][outputIndex] === null
) {
return [];
}
return connectionsData.main[outputIndex] as INodeExecutionData[];
return connectionsData[connectionType][outputIndex] as INodeExecutionData[];
},
// Returns all the binary data of all the entries
@@ -462,6 +564,7 @@ export const nodeHelpers = defineComponent({
node: string | null,
runIndex: number,
outputIndex: number,
connectionType: ConnectionTypes = NodeConnectionType.Main,
): IBinaryKeyData[] {
if (node === null) {
return [];
@@ -473,7 +576,11 @@ export const nodeHelpers = defineComponent({
return [];
}
const inputData = this.getMainInputData(runData[node][runIndex].data!, outputIndex);
const inputData = this.getInputData(
runData[node][runIndex].data!,
outputIndex,
connectionType,
);
const returnData: IBinaryKeyData[] = [];
for (let i = 0; i < inputData.length; i++) {
@@ -509,6 +616,7 @@ export const nodeHelpers = defineComponent({
this.workflowsStore.clearNodeExecutionData(node.name);
this.updateNodeParameterIssues(node);
this.updateNodeCredentialIssues(node);
this.updateNodesInputIssues();
if (trackHistory) {
this.historyStore.pushCommandToUndo(
new EnableNodeToggleCommand(node.name, oldState === true, node.disabled === true),
@@ -531,6 +639,9 @@ export const nodeHelpers = defineComponent({
if (nodeType !== null && nodeType.subtitle !== undefined) {
try {
ExpressionEvaluatorProxy.setEvaluator(
useSettingsStore().settings.expressions?.evaluator ?? 'tmpl',
);
return workflow.expression.getSimpleParameterValue(
data as INode,
nodeType.subtitle,

View File

@@ -494,7 +494,7 @@ export const pushConnection = defineComponent({
runDataExecuted.data.resultData.runData = this.workflowsStore.getWorkflowRunData;
}
this.workflowsStore.executingNode = null;
this.workflowsStore.executingNode.length = 0;
this.workflowsStore.setWorkflowExecutionData(runDataExecuted as IExecutionResponse);
this.uiStore.removeActiveAction('workflowRunning');
@@ -543,10 +543,11 @@ export const pushConnection = defineComponent({
// A node finished to execute. Add its data
const pushData = receivedData.data;
this.workflowsStore.addNodeExecutionData(pushData);
this.workflowsStore.removeExecutingNode(pushData.nodeName);
} else if (receivedData.type === 'nodeExecuteBefore') {
// A node started to be executed. Set it as executing.
const pushData = receivedData.data;
this.workflowsStore.executingNode = pushData.nodeName;
this.workflowsStore.addExecutingNode(pushData.nodeName);
} else if (receivedData.type === 'testWebhookDeleted') {
// A test-webhook was deleted
const pushData = receivedData.data;

View File

@@ -30,7 +30,7 @@ import type {
INodeProperties,
IWorkflowSettings,
} from 'n8n-workflow';
import { NodeHelpers } from 'n8n-workflow';
import { NodeConnectionType, ExpressionEvaluatorProxy, NodeHelpers } from 'n8n-workflow';
import type {
INodeTypesMaxCount,
@@ -62,9 +62,45 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
import { useUsersStore } from '@/stores/users.store';
import { useSettingsStore } from '@/stores/settings.store';
import { getWorkflowPermissions } from '@/permissions';
import type { IPermissions } from '@/permissions';
export function getParentMainInputNode(workflow: Workflow, node: INode): INode {
const nodeType = useNodeTypesStore().getNodeType(node.type);
if (nodeType) {
const outputs = NodeHelpers.getNodeOutputs(workflow, node, nodeType);
if (!!outputs.find((output) => output !== NodeConnectionType.Main)) {
// Get the first node which is connected to a non-main output
const nonMainNodesConnected = outputs?.reduce((acc, outputName) => {
const parentNodes = workflow.getChildNodes(node.name, outputName);
if (parentNodes.length > 0) {
acc.push(...parentNodes);
}
return acc;
}, [] as string[]);
if (nonMainNodesConnected.length) {
const returnNode = workflow.getNode(nonMainNodesConnected[0]);
if (returnNode === null) {
// This should theoretically never happen as the node is connected
// but who knows and it makes TS happy
throw new Error(
`The node "${nonMainNodesConnected[0]}" which is a connection of "${node.name}" could not be found!`,
);
}
// The chain of non-main nodes is potentially not finished yet so
// keep on going
return getParentMainInputNode(workflow, returnNode);
}
}
}
return node;
}
export function resolveParameter(
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
opts: {
@@ -77,10 +113,16 @@ export function resolveParameter(
): IDataObject | null {
let itemIndex = opts?.targetItem?.itemIndex || 0;
const inputName = 'main';
const activeNode = useNDVStore().activeNode;
const inputName = NodeConnectionType.Main;
let activeNode = useNDVStore().activeNode;
const workflow = getCurrentWorkflow();
// Should actually just do that for incoming data and not things like parameters
if (activeNode) {
activeNode = getParentMainInputNode(workflow, activeNode);
}
const workflowRunData = useWorkflowsStore().getWorkflowRunData;
let parentNode = workflow.getParentNodes(activeNode!.name, inputName, 1);
const executionData = useWorkflowsStore().getWorkflowExecution;
@@ -162,6 +204,10 @@ export function resolveParameter(
}
const _executeData = executeData(parentNode, activeNode!.name, inputName, runIndexCurrent);
ExpressionEvaluatorProxy.setEvaluator(
useSettingsStore().settings.expressions?.evaluator ?? 'tmpl',
);
return workflow.expression.getParameterValue(
parameter,
runExecutionData,
@@ -222,6 +268,34 @@ function getCurrentWorkflow(copyData?: boolean): Workflow {
return useWorkflowsStore().getCurrentWorkflow(copyData);
}
function getConnectedNodes(
direction: 'upstream' | 'downstream',
workflow: Workflow,
nodeName: string,
): string[] {
let checkNodes: string[];
if (direction === 'downstream') {
checkNodes = workflow.getChildNodes(nodeName);
} else if (direction === 'upstream') {
checkNodes = workflow.getParentNodes(nodeName);
} else {
throw new Error(`The direction "${direction}" is not supported!`);
}
// Find also all nodes which are connected to the child nodes via a non-main input
let connectedNodes: string[] = [];
checkNodes.forEach((checkNode) => {
connectedNodes = [
...connectedNodes,
checkNode,
...workflow.getParentNodes(checkNode, 'ALL_NON_MAIN'),
];
});
// Remove duplicates
return [...new Set(connectedNodes)];
}
function getNodes(): INodeUi[] {
return useWorkflowsStore().getNodes();
}
@@ -356,11 +430,33 @@ export function executeData(
[inputName]: workflowRunData[currentNode][runIndex].source,
};
} else {
const workflow = getCurrentWorkflow();
let previousNodeOutput: number | undefined;
// As the node can be connected through either of the outputs find the correct one
// and set it to make pairedItem work on not executed nodes
if (workflow.connectionsByDestinationNode[currentNode]?.main) {
mainConnections: for (const mainConnections of workflow.connectionsByDestinationNode[
currentNode
].main) {
for (const connection of mainConnections) {
if (
connection.type === NodeConnectionType.Main &&
connection.node === parentNodeName
) {
previousNodeOutput = connection.index;
break mainConnections;
}
}
}
}
// The current node did not get executed in UI yet so build data manually
executeData.source = {
[inputName]: [
{
previousNode: parentNodeName,
previousNodeOutput,
},
],
};
@@ -399,7 +495,9 @@ export const workflowHelpers = defineComponent({
resolveParameter,
resolveRequiredParameters,
getCurrentWorkflow,
getConnectedNodes,
getNodes,
getParentMainInputNode,
getWorkflow,
getNodeTypes,
connectionInputData,

View File

@@ -2,8 +2,8 @@ import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import type { IExecutionPushResponse, IExecutionResponse, IStartRunData } from '@/Interface';
import type { IRunData, IRunExecutionData, IWorkflowBase } from 'n8n-workflow';
import { NodeHelpers, TelemetryHelpers } from 'n8n-workflow';
import type { IRunData, IRunExecutionData, ITaskData, IWorkflowBase } from 'n8n-workflow';
import { NodeHelpers, NodeConnectionType, TelemetryHelpers } from 'n8n-workflow';
import { externalHooks } from '@/mixins/externalHooks';
import { workflowHelpers } from '@/mixins/workflowHelpers';
@@ -28,7 +28,7 @@ export const workflowRun = defineComponent({
methods: {
// Starts to executes a workflow on server.
async runWorkflowApi(runData: IStartRunData): Promise<IExecutionPushResponse> {
if (this.rootStore.pushConnectionActive === false) {
if (!this.rootStore.pushConnectionActive) {
// Do not start if the connection to server is not active
// because then it can not receive the data as it executes.
throw new Error(this.$locale.baseText('workflowRun.noActiveConnectionToTheServer'));
@@ -57,9 +57,12 @@ export const workflowRun = defineComponent({
return response;
},
async runWorkflow(
nodeName?: string,
source?: string,
options:
| { destinationNode: string; source?: string }
| { triggerNode: string; nodeData: ITaskData; source?: string }
| { source?: string },
): Promise<IExecutionPushResponse | undefined> {
const workflow = this.getCurrentWorkflow();
@@ -74,9 +77,9 @@ export const workflowRun = defineComponent({
try {
// Check first if the workflow has any issues before execute it
const issuesExist = this.workflowsStore.nodesIssuesExist;
if (issuesExist === true) {
if (issuesExist) {
// If issues exist get all of the issues of all nodes
const workflowIssues = this.checkReadyForExecution(workflow, nodeName);
const workflowIssues = this.checkReadyForExecution(workflow, options.destinationNode);
if (workflowIssues !== null) {
const errorMessages = [];
let nodeIssues: string[];
@@ -115,13 +118,17 @@ export const workflowRun = defineComponent({
duration: 0,
});
this.titleSet(workflow.name as string, 'ERROR');
void this.$externalHooks().run('workflowRun.runError', { errorMessages, nodeName });
void this.$externalHooks().run('workflowRun.runError', {
errorMessages,
nodeName: options.destinationNode,
});
await this.getWorkflowDataToSave().then((workflowData) => {
this.$telemetry.track('Workflow execution preflight failed', {
workflow_id: workflow.id,
workflow_name: workflow.name,
execution_type: nodeName ? 'node' : 'workflow',
execution_type:
options.destinationNode || options.triggerNode ? 'node' : 'workflow',
node_graph_string: JSON.stringify(
TelemetryHelpers.generateNodesGraph(
workflowData as IWorkflowBase,
@@ -138,8 +145,12 @@ export const workflowRun = defineComponent({
// Get the direct parents of the node
let directParentNodes: string[] = [];
if (nodeName !== undefined) {
directParentNodes = workflow.getParentNodes(nodeName, 'main', 1);
if (options.destinationNode !== undefined) {
directParentNodes = workflow.getParentNodes(
options.destinationNode,
NodeConnectionType.Main,
1,
);
}
const runData = this.workflowsStore.getWorkflowRunData;
@@ -155,7 +166,7 @@ export const workflowRun = defineComponent({
for (const directParentNode of directParentNodes) {
// Go over the parents of that node so that we can get a start
// node for each of the branches
const parentNodes = workflow.getParentNodes(directParentNode, 'main');
const parentNodes = workflow.getParentNodes(directParentNode, NodeConnectionType.Main);
// Add also the enabled direct parent to be checked
if (workflow.nodes[directParentNode].disabled) continue;
@@ -181,8 +192,22 @@ export const workflowRun = defineComponent({
}
}
if (startNodes.length === 0 && nodeName !== undefined) {
startNodes.push(nodeName);
let executedNode: string | undefined;
if (
startNodes.length === 0 &&
'destinationNode' in options &&
options.destinationNode !== undefined
) {
executedNode = options.destinationNode;
startNodes.push(options.destinationNode);
} else if ('triggerNode' in options && 'nodeData' in options) {
startNodes.push(
...workflow.getChildNodes(options.triggerNode, NodeConnectionType.Main, 1),
);
newRunData = {
[options.triggerNode]: [options.nodeData],
};
executedNode = options.triggerNode;
}
if (this.workflowsStore.isNewWorkflow) {
@@ -197,8 +222,8 @@ export const workflowRun = defineComponent({
pinData: workflowData.pinData,
startNodes,
};
if (nodeName) {
startRunData.destinationNode = nodeName;
if ('destinationNode' in options) {
startRunData.destinationNode = options.destinationNode;
}
// Init the execution data to represent the start of the execution
@@ -211,7 +236,7 @@ export const workflowRun = defineComponent({
startedAt: new Date(),
stoppedAt: undefined,
workflowId: workflow.id,
executedNode: nodeName,
executedNode,
data: {
resultData: {
runData: newRunData || {},
@@ -234,7 +259,10 @@ export const workflowRun = defineComponent({
const runWorkflowApiResponse = await this.runWorkflowApi(startRunData);
await this.$externalHooks().run('workflowRun.runWorkflow', { nodeName, source });
await this.$externalHooks().run('workflowRun.runWorkflow', {
nodeName: options.destinationNode,
source: options.source,
});
return runWorkflowApiResponse;
} catch (error) {