mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-01 15:47:41 +08:00
fix(core): Resolve $().item expressions in partial executions (#27338)
This commit is contained in:
committed by
GitHub
parent
c114e7ea59
commit
d3e45bc126
@@ -1229,6 +1229,24 @@ export class WorkflowDataProxy {
|
||||
if (pinnedData) {
|
||||
return that.returnExecutionData(pinnedData[itemIndex], resolveFullItem);
|
||||
}
|
||||
// The active node has not run yet (e.g. partial execution),
|
||||
// so paired item resolution cannot trace through the chain.
|
||||
// Fall back to reading directly from the referenced node's
|
||||
// run data, which is what first()/last()/all() do.
|
||||
const nodeRunData = that.getNodeExecutionOrPinnedData({
|
||||
nodeName,
|
||||
});
|
||||
if (nodeRunData.length) {
|
||||
// In the UI preview itemIndex is always 0, but guard against
|
||||
// out-of-bounds access in case that assumption ever changes.
|
||||
if (itemIndex >= nodeRunData.length) {
|
||||
throw createExpressionError(
|
||||
`"${nodeName}" node has ${nodeRunData.length} item(s) but expression references item ${itemIndex}`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
return that.returnExecutionData(nodeRunData[itemIndex], resolveFullItem);
|
||||
}
|
||||
}
|
||||
|
||||
const executionData = that.connectionInputData;
|
||||
|
||||
@@ -1907,4 +1907,141 @@ describe('WorkflowDataProxy', () => {
|
||||
expect(result).not.toHaveProperty('params');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Partial execution: $() referencing executed node from unexecuted active node', () => {
|
||||
// Scenario: Reference → Edit → NoOp → Edit Fields
|
||||
// Only Reference and Edit have been executed (partial execution).
|
||||
// Edit Fields (active, unexecuted) uses $('Edit').item.json.test
|
||||
// "Edit" has data in runData, so the expression should resolve.
|
||||
const workflowData: IWorkflowBase = {
|
||||
id: '123',
|
||||
name: 'partial execution test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
name: 'Reference',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [256, 16] as [number, number],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
name: 'Edit',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.4,
|
||||
position: [544, 16] as [number, number],
|
||||
parameters: {
|
||||
assignments: {
|
||||
assignments: [
|
||||
{
|
||||
id: 'e8d2af0b-147a-4b0d-a106-5ed5a5b753e4',
|
||||
name: 'test',
|
||||
value: '={{ 1111 }}',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
name: 'NoOp',
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
typeVersion: 1,
|
||||
position: [832, 32] as [number, number],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'node4',
|
||||
name: 'Edit Fields',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.4,
|
||||
position: [1136, 0] as [number, number],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Reference: {
|
||||
main: [[{ node: 'Edit', type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
Edit: {
|
||||
main: [[{ node: 'NoOp', type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
NoOp: {
|
||||
main: [[{ node: 'Edit Fields', type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
activeVersionId: null,
|
||||
isArchived: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
// Exact run data from a real partial execution (only Reference and Edit ran)
|
||||
const run: IRun = {
|
||||
data: createRunExecutionData({
|
||||
resultData: {
|
||||
runData: {
|
||||
Reference: [
|
||||
{
|
||||
startTime: 1774006769741,
|
||||
executionTime: 1,
|
||||
executionIndex: 0,
|
||||
executionStatus: 'success',
|
||||
source: [],
|
||||
data: {
|
||||
main: [[{ json: {}, pairedItem: { item: 0 } }]],
|
||||
},
|
||||
},
|
||||
],
|
||||
Edit: [
|
||||
{
|
||||
startTime: 1774006769743,
|
||||
executionTime: 8,
|
||||
executionIndex: 1,
|
||||
executionStatus: 'success',
|
||||
source: [
|
||||
{
|
||||
previousNode: 'Reference',
|
||||
previousNodeOutput: 0,
|
||||
previousNodeRun: 0,
|
||||
},
|
||||
],
|
||||
data: {
|
||||
main: [[{ json: { test: '1111' }, pairedItem: { item: 0 } }]],
|
||||
},
|
||||
},
|
||||
],
|
||||
// NoOp and Edit Fields have NOT been executed
|
||||
},
|
||||
},
|
||||
}),
|
||||
mode: 'manual',
|
||||
startedAt: new Date(),
|
||||
status: 'success',
|
||||
storedAt: 'db',
|
||||
};
|
||||
|
||||
test('$("Edit").first() should return data when referenced node was executed', () => {
|
||||
const proxy = getProxyFromFixture(workflowData, run, 'Edit Fields', 'manual');
|
||||
expect(proxy.$('Edit').first().json.test).toBe('1111');
|
||||
});
|
||||
|
||||
test('$("Edit").isExecuted should return true', () => {
|
||||
const proxy = getProxyFromFixture(workflowData, run, 'Edit Fields', 'manual');
|
||||
expect(proxy.$('Edit').isExecuted).toBe(true);
|
||||
});
|
||||
|
||||
test('$("Edit").item.json.test should resolve when the referenced node was executed', () => {
|
||||
// .item uses paired item resolution which requires connectionInputData
|
||||
// (the active node's input). In a partial execution the active node hasn't
|
||||
// run so connectionInputData is empty, but the referenced node "Edit" has
|
||||
// data in runData. The fix falls back to reading from runData directly.
|
||||
const proxy = getProxyFromFixture(workflowData, run, 'Edit Fields', 'manual');
|
||||
expect(proxy.$('Edit').item.json.test).toBe('1111');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user