diff --git a/packages/frontend/editor-ui/src/utils/pairedItemUtils.test.ts b/packages/frontend/editor-ui/src/utils/pairedItemUtils.test.ts index e3e2835bb0..b3d2220800 100644 --- a/packages/frontend/editor-ui/src/utils/pairedItemUtils.test.ts +++ b/packages/frontend/editor-ui/src/utils/pairedItemUtils.test.ts @@ -5,6 +5,7 @@ import { getPairedItemsMapping, MAX_ITEM_COUNT_FOR_PAIRING, } from './pairedItemUtils'; +import { type ITaskData } from 'n8n-workflow'; const MOCK_EXECUTION: Partial = { data: { @@ -396,5 +397,41 @@ describe('pairedItemUtils', () => { expect(getPairedItemsMapping(mockExecution)).toEqual({}); }); + + it('should abort mapping and return empty object if execution has too many pairs', () => { + const nodeCount = 10; + const runCount = 3; + const itemCountPerRun = 3; + const pairedItemCount = 3; + const mockExecution: Partial = { + data: { + resultData: { + runData: Object.fromEntries( + Array.from({ length: nodeCount }).map<[string, ITaskData[]]>((_, j) => [ + `node_${j}`, + Array.from({ length: runCount }).map(() => ({ + startTime: 1706027170005, + executionTime: 0, + source: j === 0 ? [] : [{ previousNode: `node_${j - 1}` }], + executionStatus: 'success', + data: { + main: [ + Array.from({ length: itemCountPerRun }).map(() => ({ + json: {}, + pairedItem: Array.from({ length: pairedItemCount }).map((__, i) => ({ + item: i, + })), + })), + ], + }, + })), + ]), + ), + }, + }, + }; + + expect(getPairedItemsMapping(mockExecution)).toEqual({}); + }); }); }); diff --git a/packages/frontend/editor-ui/src/utils/pairedItemUtils.ts b/packages/frontend/editor-ui/src/utils/pairedItemUtils.ts index 786e46fe45..e4b7c497fe 100644 --- a/packages/frontend/editor-ui/src/utils/pairedItemUtils.ts +++ b/packages/frontend/editor-ui/src/utils/pairedItemUtils.ts @@ -1,9 +1,16 @@ -import type { IPairedItemData, IRunData, ITaskData } from 'n8n-workflow'; +import { type IPairedItemData, type IRunData, type ITaskData } from 'n8n-workflow'; import type { IExecutionResponse, TargetItem } from '@/Interface'; import { isNotNull } from '@/utils/typeGuards'; export const MAX_ITEM_COUNT_FOR_PAIRING = 1000; +const MAX_PAIR_COUNT = 100000; + +interface Paths { + data: { [item: string]: string[][] }; + size: number; +} + /* Utility functions that provide shared functionalities used to add paired item support to nodes */ @@ -51,29 +58,36 @@ export function getSourceItems( } function addPairing( - paths: { [item: string]: string[][] }, + paths: Paths, pairedItemId: string, pairedItem: IPairedItemData, sources: ITaskData['source'], ) { - paths[pairedItemId] = paths[pairedItemId] || []; + if (paths.size >= MAX_PAIR_COUNT) { + throw Error(); + } + + paths.data[pairedItemId] = paths.data[pairedItemId] || []; const input = pairedItem.input || 0; const sourceNode = sources[input]?.previousNode; if (!sourceNode) { // trigger nodes for example - paths[pairedItemId].push([pairedItemId]); + paths.data[pairedItemId].push([pairedItemId]); + paths.size++; return; } const sourceNodeOutput = sources[input]?.previousNodeOutput || 0; const sourceNodeRun = sources[input]?.previousNodeRun || 0; const sourceItem = getPairedItemId(sourceNode, sourceNodeRun, sourceNodeOutput, pairedItem.item); - if (!paths[sourceItem]) { - paths[sourceItem] = [[sourceItem]]; // pinned data case + if (!paths.data[sourceItem]) { + paths.data[sourceItem] = [[sourceItem]]; // pinned data case + paths.size++; } - paths[sourceItem]?.forEach((path) => { - paths?.[pairedItemId]?.push([...path, pairedItemId]); + paths.data[sourceItem]?.forEach((path) => { + paths.data[pairedItemId]?.push([...path, pairedItemId]); + paths.size++; }); } @@ -82,7 +96,7 @@ function addPairedItemIdsRec( runIndex: number, runData: IRunData, seen: Set, - paths: { [item: string]: string[][] }, + paths: Paths, pinned: Set, ) { const key = `${node}_r${runIndex}`; @@ -128,7 +142,7 @@ function addPairedItemIdsRec( outputData.forEach((executionData, item: number) => { const pairedItemId = getPairedItemId(node, runIndex, output, item); if (!executionData.pairedItem) { - paths[pairedItemId] = []; + paths.data[pairedItemId] = []; return; } @@ -150,11 +164,11 @@ function addPairedItemIdsRec( }); } -function getMapping(paths: { [item: string]: string[][] }): { [item: string]: Set } { +function getMapping(paths: Paths): { [item: string]: Set } { const mapping: { [itemId: string]: Set } = {}; - Object.keys(paths).forEach((item) => { - paths?.[item]?.forEach((path) => { + Object.keys(paths.data).forEach((item) => { + paths.data[item]?.forEach((path) => { path.forEach((otherItem) => { if (otherItem !== item) { mapping[otherItem] = mapping[otherItem] || new Set(); @@ -205,15 +219,20 @@ export function getPairedItemsMapping(executionResponse: Partial(); - const pinned = new Set(Object.keys(executionResponse.data.resultData.pinData || {})); + const paths: Paths = { size: 0, data: {} }; - const paths: { [item: string]: string[][] } = {}; - Object.keys(runData).forEach((node) => { - runData[node].forEach((_, runIndex: number) => { - addPairedItemIdsRec(node, runIndex, runData, seen, paths, pinned); + try { + const seen = new Set(); + const pinned = new Set(Object.keys(executionResponse.data.resultData.pinData ?? {})); + + Object.keys(runData).forEach((node) => { + runData[node].forEach((_, runIndex: number) => { + addPairedItemIdsRec(node, runIndex, runData, seen, paths, pinned); + }); }); - }); + } catch { + return {}; + } return getMapping(paths); }