fix(editor): Fix unnecessary execution of nodes when there is pin data (#8567)

Co-authored-by: Omar Ajoue <krynble@gmail.com>
This commit is contained in:
Csaba Tuncsik 2024-02-16 17:24:07 +01:00 committed by GitHub
parent 9c0fe413d9
commit 46fe544b9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 224 additions and 41 deletions

View file

@ -489,4 +489,31 @@ describe('Execution', () => {
.should('have.class', 'has-run');
});
});
it.only('should send proper payload for node rerun', () => {
cy.createFixtureWorkflow(
'Multiple_trigger_node_rerun.json',
`Multiple trigger node rerun ${uuid()}`,
);
workflowPage.getters.zoomToFitButton().click();
workflowPage.getters.executeWorkflowButton().click();
workflowPage.getters.clearExecutionDataButton().should('be.visible');
cy.intercept('POST', '/rest/workflows/run').as('workflowRun');
workflowPage.getters
.canvasNodeByName('do something with them')
.findChildByTestId('execute-node-button')
.click({ force: true });
cy.wait('@workflowRun').then((interception) => {
expect(interception.request.body).to.have.property('runData').that.is.an('object');
const expectedKeys = ['When clicking "Test workflow"', 'fetch 5 random users'];
expect(Object.keys(interception.request.body.runData)).to.have.lengthOf(expectedKeys.length);
expect(interception.request.body.runData).to.include.all.keys(expectedKeys);
});
});
});

View file

@ -0,0 +1,133 @@
{
"name": "Multiple trigger node rerun",
"nodes": [
{
"parameters": {},
"id": "5ae8991f-08a2-4b27-b61c-85e3b8a83693",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
460,
460
]
},
{
"parameters": {
"url": "https://random-data-api.com/api/v2/users?size=5",
"options": {}
},
"id": "22511d75-ab54-49e1-b8af-08b8b3372373",
"name": "fetch 5 random users",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
680,
460
]
},
{
"parameters": {
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.first_name_reversed = item.json = {\n firstName: item.json.first_name,\n firstnNameReversed: item.json.first_name_BUG.split(\"\").reverse().join(\"\")\n };\n}\n\nreturn $input.all();"
},
"id": "4b66b15a-1685-46c1-a5e3-ebf8cdb11d21",
"name": "do something with them",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
460
]
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "* * * * *"
}
]
}
},
"id": "d763fc3b-6c4a-4d39-8857-ff84f7b6dc83",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.1,
"position": [
460,
660
]
}
],
"pinData": {
"Schedule Trigger": [
{
"json": {
"timestamp": "2024-01-29T13:45:00.006+01:00",
"Readable date": "January 29th 2024, 1:45:00 pm",
"Readable time": "1:45:00 pm",
"Day of week": "Monday",
"Year": "2024",
"Month": "January",
"Day of month": "29",
"Hour": "13",
"Minute": "45",
"Second": "00",
"Timezone": "CET +01:00"
}
}
],
"When clicking \"Test workflow\"": [
{
"json": {}
}
]
},
"connections": {
"When clicking \"Test workflow\"": {
"main": [
[
{
"node": "fetch 5 random users",
"type": "main",
"index": 0
}
]
]
},
"fetch 5 random users": {
"main": [
[
{
"node": "do something with them",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "fetch 5 random users",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "b9a6c3b0-15cd-4359-a92e-12a691a36b7b",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "8a47b83b4479b11330fdf21ccc96d4a8117035a968612e452b4c87bfd09c16c7"
},
"id": "PymcwIrbqgNh3O0K",
"tags": []
}

View file

@ -7,7 +7,9 @@ import type {
IRunData,
IRunExecutionData,
ITaskData,
IPinData,
IWorkflowBase,
Workflow,
} from 'n8n-workflow';
import {
NodeHelpers,
@ -29,6 +31,55 @@ import { useExternalHooks } from '@/composables/useExternalHooks';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useRouter } from 'vue-router';
export const consolidateRunDataAndStartNodes = (
directParentNodes: string[],
runData: IRunData | null,
pinData: IPinData | undefined,
workflow: Workflow,
): { runData: IRunData | undefined; startNodes: string[] } => {
const startNodes: string[] = [];
let newRunData: IRunData | undefined;
if (runData !== null && Object.keys(runData).length !== 0) {
newRunData = {};
// Go over the direct parents of the node
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, NodeConnectionType.Main);
// Add also the enabled direct parent to be checked
if (workflow.nodes[directParentNode].disabled) continue;
parentNodes.push(directParentNode);
for (const parentNode of parentNodes) {
if (
(runData[parentNode] === undefined || runData[parentNode].length === 0) &&
pinData?.[parentNode].length === 0
) {
// When we hit a node which has no data we stop and set it
// as a start node the execution from and then go on with other
// direct input nodes
startNodes.push(parentNode);
break;
}
if (runData[parentNode] !== undefined) {
newRunData[parentNode] = runData[parentNode]?.slice(0, 1);
}
}
}
if (Object.keys(newRunData).length === 0) {
// If there is no data for any of the parent nodes make sure
// that run data is empty that it runs regularly
newRunData = undefined;
}
}
return { runData: newRunData, startNodes };
};
export const workflowRun = defineComponent({
setup() {
const nodeHelpers = useNodeHelpers();
@ -181,43 +232,21 @@ export const workflowRun = defineComponent({
const runData = this.workflowsStore.getWorkflowRunData;
let newRunData: IRunData | undefined;
const startNodes: string[] = [];
if (runData !== null && Object.keys(runData).length !== 0) {
newRunData = {};
// Go over the direct parents of the node
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, NodeConnectionType.Main);
// Add also the enabled direct parent to be checked
if (workflow.nodes[directParentNode].disabled) continue;
parentNodes.push(directParentNode);
for (const parentNode of parentNodes) {
if (runData[parentNode] === undefined || runData[parentNode].length === 0) {
// When we hit a node which has no data we stop and set it
// as a start node the execution from and then go on with other
// direct input nodes
startNodes.push(parentNode);
break;
}
newRunData[parentNode] = runData[parentNode].slice(0, 1);
}
}
if (Object.keys(newRunData).length === 0) {
// If there is no data for any of the parent nodes make sure
// that run data is empty that it runs regularly
newRunData = undefined;
}
if (this.workflowsStore.isNewWorkflow) {
await this.workflowHelpers.saveCurrentWorkflow();
}
const workflowData = await this.workflowHelpers.getWorkflowDataToSave();
const consolidatedData = consolidateRunDataAndStartNodes(
directParentNodes,
runData,
workflowData.pinData,
workflow,
);
const { startNodes } = consolidatedData;
let { runData: newRunData } = consolidatedData;
let executedNode: string | undefined;
if (
startNodes.length === 0 &&
@ -236,12 +265,6 @@ export const workflowRun = defineComponent({
executedNode = options.triggerNode;
}
if (this.workflowsStore.isNewWorkflow) {
await this.workflowHelpers.saveCurrentWorkflow();
}
const workflowData = await this.workflowHelpers.getWorkflowDataToSave();
const startRunData: IStartRunData = {
workflowData,
runData: newRunData,