From 87b3c508b3d5a7d6f3b9f8377de66567a04fa970 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Wed, 19 Feb 2025 17:33:26 +0200 Subject: [PATCH] fix(Code Node): Fix `$items` in Code node when using task runner (#13368) --- .../__tests__/js-task-runner.test.ts | 2 +- .../__tests__/built-ins-parser.test.ts | 17 ++++++++ .../built-ins-parser/built-ins-parser.ts | 42 ++++++++++++++++++- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts index a666240098..1615b54725 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts @@ -850,7 +850,7 @@ describe('JsTaskRunner', () => { }); }); - test.each([['items'], ['$input.all()'], ["$('Trigger').all()"]])( + test.each([['items'], ['$input.all()'], ["$('Trigger').all()"], ['$items()']])( 'should have all input items in the context as %s', async (expression) => { const outcome = await executeForAllItems({ diff --git a/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/__tests__/built-ins-parser.test.ts b/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/__tests__/built-ins-parser.test.ts index 1f839b38f2..7125671b62 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/__tests__/built-ins-parser.test.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/__tests__/built-ins-parser.test.ts @@ -144,6 +144,23 @@ describe('BuiltInsParser', () => { ); }); + describe('$items(...)', () => { + it('should mark input as needed when $items() is used without arguments', () => { + const state = parseAndExpectOk('$items()'); + expect(state).toEqual(new BuiltInsParserState({ needs$input: true })); + }); + + it('should require the given node when $items() is used with a static value', () => { + const state = parseAndExpectOk('$items("nodeName")'); + expect(state).toEqual(new BuiltInsParserState({ neededNodeNames: new Set(['nodeName']) })); + }); + + it('should require all nodes when $items() is used with a variable', () => { + const state = parseAndExpectOk('var n = "name"; $items(n)'); + expect(state).toEqual(new BuiltInsParserState({ needsAllNodes: true, needs$input: true })); + }); + }); + describe('$node', () => { it('should require all nodes when $node is used', () => { const state = parseAndExpectOk('return $node["name"];'); diff --git a/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/built-ins-parser.ts b/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/built-ins-parser.ts index fea0d94469..aec0d4f63d 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/built-ins-parser.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/built-ins-parser.ts @@ -54,8 +54,22 @@ export class BuiltInsParser { ) => { // $(...) const isDollar = node.callee.type === 'Identifier' && node.callee.name === '$'; - if (!isDollar) return; + const isItems = node.callee.type === 'Identifier' && node.callee.name === '$items'; + if (isDollar) { + this.visitDollarCallExpression(node, state, ancestors); + } else if (isItems) { + // $items(...) is a legacy syntax that is not documented but we still + // need to support it for backwards compatibility + this.visitDollarItemsCallExpression(node, state); + } + }; + /** $(...) */ + private visitDollarCallExpression( + node: CallExpression, + state: BuiltInsParserState, + ancestors: Node[], + ) { // $(): This is not valid, ignore if (node.arguments.length === 0) { return; @@ -78,7 +92,31 @@ export class BuiltInsParser { // Determine how $("node") is used this.handlePrevNodeCall(node, state, ancestors); - }; + } + + /** $items(...) */ + private visitDollarItemsCallExpression(node: CallExpression, state: BuiltInsParserState) { + // $items(): This gets items from the previous node + if (node.arguments.length === 0) { + state.markInputAsNeeded(); + return; + } + + const firstArg = node.arguments[0]; + if (!isLiteral(firstArg)) { + // $items(variable): Can't easily determine statically, mark all nodes as needed + state.markNeedsAllNodes(); + return; + } + + if (typeof firstArg.value !== 'string') { + // $items(123): Static value, but not a string --> unsupported code --> ignore + return; + } + + // $items(nodeName): Static value, mark 'nodeName' as needed + state.markNodeAsNeeded(firstArg.value); + } private handlePrevNodeCall(_node: CallExpression, state: BuiltInsParserState, ancestors: Node[]) { // $("node").item, .pairedItem or .itemMatching: In a case like this, the execution