mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(editor): Update node execution itemCount to support multiple outputs (no-changelog) (#19646)
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { nodeExecuteAfter } from './nodeExecuteAfter';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useAssistantStore } from '@/stores/assistant.store';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import type { NodeExecuteAfter } from '@n8n/api-types/push/execution';
|
||||
import { TRIMMED_TASK_DATA_CONNECTIONS_KEY } from 'n8n-workflow';
|
||||
|
||||
describe('nodeExecuteAfter', () => {
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia({
|
||||
stubActions: true,
|
||||
});
|
||||
|
||||
setActivePinia(pinia);
|
||||
});
|
||||
|
||||
it('should update node execution data with placeholder and remove executing node', async () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const assistantStore = mockedStore(useAssistantStore);
|
||||
|
||||
const event: NodeExecuteAfter = {
|
||||
type: 'nodeExecuteAfter',
|
||||
data: {
|
||||
executionId: 'exec-1',
|
||||
nodeName: 'Test Node',
|
||||
itemCountByConnectionType: { main: [2, 1] },
|
||||
data: {
|
||||
executionTime: 100,
|
||||
startTime: 1234567890,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfter(event);
|
||||
|
||||
expect(workflowsStore.updateNodeExecutionData).toHaveBeenCalledTimes(1);
|
||||
expect(workflowsStore.removeExecutingNode).toHaveBeenCalledTimes(1);
|
||||
expect(workflowsStore.removeExecutingNode).toHaveBeenCalledWith('Test Node');
|
||||
expect(assistantStore.onNodeExecution).toHaveBeenCalledTimes(1);
|
||||
expect(assistantStore.onNodeExecution).toHaveBeenCalledWith(event.data);
|
||||
|
||||
// Verify the placeholder data structure
|
||||
const updateCall = workflowsStore.updateNodeExecutionData.mock.calls[0][0];
|
||||
expect(updateCall.data.data).toEqual({
|
||||
main: [
|
||||
Array.from({ length: 2 }).fill({ json: { [TRIMMED_TASK_DATA_CONNECTIONS_KEY]: true } }),
|
||||
Array.from({ length: 1 }).fill({ json: { [TRIMMED_TASK_DATA_CONNECTIONS_KEY]: true } }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple connection types', async () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
|
||||
const event: NodeExecuteAfter = {
|
||||
type: 'nodeExecuteAfter',
|
||||
data: {
|
||||
executionId: 'exec-1',
|
||||
nodeName: 'Test Node',
|
||||
itemCountByConnectionType: {
|
||||
main: [3],
|
||||
ai_memory: [1, 2],
|
||||
ai_tool: [1],
|
||||
},
|
||||
data: {
|
||||
executionTime: 100,
|
||||
startTime: 1234567890,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfter(event);
|
||||
|
||||
const updateCall = workflowsStore.updateNodeExecutionData.mock.calls[0][0];
|
||||
expect(updateCall.data.data).toEqual({
|
||||
main: [
|
||||
Array.from({ length: 3 }).fill({ json: { [TRIMMED_TASK_DATA_CONNECTIONS_KEY]: true } }),
|
||||
],
|
||||
ai_memory: [
|
||||
Array.from({ length: 1 }).fill({ json: { [TRIMMED_TASK_DATA_CONNECTIONS_KEY]: true } }),
|
||||
Array.from({ length: 2 }).fill({ json: { [TRIMMED_TASK_DATA_CONNECTIONS_KEY]: true } }),
|
||||
],
|
||||
ai_tool: [
|
||||
Array.from({ length: 1 }).fill({ json: { [TRIMMED_TASK_DATA_CONNECTIONS_KEY]: true } }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty itemCountByConnectionType', async () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
|
||||
const event: NodeExecuteAfter = {
|
||||
type: 'nodeExecuteAfter',
|
||||
data: {
|
||||
executionId: 'exec-1',
|
||||
nodeName: 'Test Node',
|
||||
itemCountByConnectionType: {},
|
||||
data: {
|
||||
executionTime: 100,
|
||||
startTime: 1234567890,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfter(event);
|
||||
|
||||
const updateCall = workflowsStore.updateNodeExecutionData.mock.calls[0][0];
|
||||
expect(updateCall.data.data).toEqual({
|
||||
main: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve original data structure except for data property', async () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
|
||||
const event: NodeExecuteAfter = {
|
||||
type: 'nodeExecuteAfter',
|
||||
data: {
|
||||
executionId: 'exec-1',
|
||||
nodeName: 'Test Node',
|
||||
itemCountByConnectionType: { main: [1] },
|
||||
data: {
|
||||
executionTime: 100,
|
||||
startTime: 1234567890,
|
||||
executionIndex: 0,
|
||||
source: [null],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfter(event);
|
||||
|
||||
const updateCall = workflowsStore.updateNodeExecutionData.mock.calls[0][0];
|
||||
expect(updateCall.executionId).toBe('exec-1');
|
||||
expect(updateCall.nodeName).toBe('Test Node');
|
||||
expect(updateCall.data.executionTime).toBe(100);
|
||||
expect(updateCall.data.startTime).toBe(1234567890);
|
||||
expect(updateCall.data.executionIndex).toBe(0);
|
||||
expect(updateCall.data.source).toEqual([null]);
|
||||
|
||||
// Only the data property should be replaced with placeholder
|
||||
expect(updateCall.data.data).toEqual({
|
||||
main: [
|
||||
Array.from({ length: 1 }).fill({ json: { [TRIMMED_TASK_DATA_CONNECTIONS_KEY]: true } }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter out invalid connection types', async () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
|
||||
const event: NodeExecuteAfter = {
|
||||
type: 'nodeExecuteAfter',
|
||||
data: {
|
||||
executionId: 'exec-1',
|
||||
nodeName: 'Test Node',
|
||||
itemCountByConnectionType: {
|
||||
main: [1],
|
||||
// @ts-expect-error Testing invalid connection type
|
||||
invalid_connection: [2], // This should be filtered out by isValidNodeConnectionType
|
||||
},
|
||||
data: {
|
||||
executionTime: 100,
|
||||
startTime: 1234567890,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await nodeExecuteAfter(event);
|
||||
|
||||
const updateCall = workflowsStore.updateNodeExecutionData.mock.calls[0][0];
|
||||
// Should only contain main connection, invalid_connection should be filtered out
|
||||
expect(updateCall.data.data).toEqual({
|
||||
main: [
|
||||
Array.from({ length: 1 }).fill({ json: { [TRIMMED_TASK_DATA_CONNECTIONS_KEY]: true } }),
|
||||
],
|
||||
});
|
||||
expect(updateCall.data.data?.invalid_connection).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { NodeExecuteAfter } from '@n8n/api-types/push/execution';
|
||||
import { useAssistantStore } from '@/stores/assistant.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { ITaskData } from 'n8n-workflow';
|
||||
import type { INodeExecutionData, ITaskData } from 'n8n-workflow';
|
||||
import { TRIMMED_TASK_DATA_CONNECTIONS_KEY } from 'n8n-workflow';
|
||||
import type { PushPayload } from '@n8n/api-types';
|
||||
import { isValidNodeConnectionType } from '@/utils/typeGuards';
|
||||
|
||||
/**
|
||||
* Handles the 'nodeExecuteAfter' event, which happens after a node is executed.
|
||||
@@ -18,15 +19,23 @@ export async function nodeExecuteAfter({ data: pushData }: NodeExecuteAfter) {
|
||||
* a placeholder object indicating that the data has been trimmed until the
|
||||
* `nodeExecuteAfterData` event comes in.
|
||||
*/
|
||||
|
||||
const placeholderOutputData: ITaskData['data'] = {
|
||||
main: [],
|
||||
};
|
||||
|
||||
if (typeof pushData.itemCount === 'number') {
|
||||
const fillObject = { json: { [TRIMMED_TASK_DATA_CONNECTIONS_KEY]: true } };
|
||||
const fillArray = new Array(pushData.itemCount).fill(fillObject);
|
||||
|
||||
placeholderOutputData.main = [fillArray];
|
||||
if (
|
||||
pushData.itemCountByConnectionType &&
|
||||
typeof pushData.itemCountByConnectionType === 'object'
|
||||
) {
|
||||
const fillObject: INodeExecutionData = { json: { [TRIMMED_TASK_DATA_CONNECTIONS_KEY]: true } };
|
||||
for (const [connectionType, outputs] of Object.entries(pushData.itemCountByConnectionType)) {
|
||||
if (isValidNodeConnectionType(connectionType)) {
|
||||
placeholderOutputData[connectionType] = outputs.map((count) =>
|
||||
Array.from({ length: count }, () => fillObject),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pushDataWithPlaceholderOutputData: PushPayload<'nodeExecuteAfterData'> = {
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('nodeExecuteAfterData', () => {
|
||||
data: {
|
||||
executionId: 'exec-1',
|
||||
nodeName: 'Test Node',
|
||||
itemCount: 1,
|
||||
itemCountByConnectionType: { main: [1] },
|
||||
data: {
|
||||
executionTime: 0,
|
||||
startTime: 0,
|
||||
|
||||
@@ -330,7 +330,7 @@ describe('LogsPanel', () => {
|
||||
workflowsStore.updateNodeExecutionData({
|
||||
nodeName: 'AI Agent',
|
||||
executionId: '567',
|
||||
itemCount: 1,
|
||||
itemCountByConnectionType: { ai_agent: [1] },
|
||||
data: {
|
||||
executionIndex: 0,
|
||||
startTime: Date.parse('2025-04-20T12:34:51.000Z'),
|
||||
|
||||
@@ -1409,7 +1409,7 @@ function generateMockExecutionEvents() {
|
||||
const successEvent: PushPayload<'nodeExecuteAfter'> = {
|
||||
executionId: '59',
|
||||
nodeName: 'When clicking ‘Execute workflow’',
|
||||
itemCount: 1,
|
||||
itemCountByConnectionType: { main: [1] },
|
||||
data: {
|
||||
hints: [],
|
||||
startTime: 1727867966633,
|
||||
|
||||
Reference in New Issue
Block a user