mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
fix(core): Throw on bare OutputSelector passed to .add()/.to() (#29736)
This commit is contained in:
parent
04e9b258a8
commit
60a51229e0
|
|
@ -296,6 +296,82 @@ describe('Workflow Builder', () => {
|
|||
'Cannot call .input() on the workflow builder',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw a clear error when an OutputSelector is passed to .add()', () => {
|
||||
const t = trigger({ type: 'n8n-nodes-base.webhookTrigger', version: 1, config: {} });
|
||||
const source = node({
|
||||
type: 'n8n-nodes-base.if',
|
||||
version: 2,
|
||||
config: { name: 'Branch' },
|
||||
});
|
||||
const target = node({
|
||||
type: 'n8n-nodes-base.set',
|
||||
version: 3,
|
||||
config: { name: 'Set' },
|
||||
});
|
||||
|
||||
const wf = workflow('test-id', 'Test Workflow').add(t);
|
||||
const misuse = source.output(0) as unknown as NodeInstance<string, string, unknown>;
|
||||
expect(() => wf.add(misuse).to(target)).toThrow(TypeError);
|
||||
expect(() => wf.add(misuse).to(target)).toThrow(/Cannot pass an OutputSelector to \.add\(\)/);
|
||||
expect(() => wf.add(misuse).to(target)).toThrow(/Branch\.output\(0\)\.to\(/);
|
||||
});
|
||||
|
||||
it('should throw a clear error when an OutputSelector is passed to .to()', () => {
|
||||
const t = trigger({ type: 'n8n-nodes-base.webhookTrigger', version: 1, config: {} });
|
||||
const source = node({
|
||||
type: 'n8n-nodes-base.if',
|
||||
version: 2,
|
||||
config: { name: 'Branch' },
|
||||
});
|
||||
|
||||
const wf = workflow('test-id', 'Test Workflow').add(t);
|
||||
const misuse = source.output(1) as unknown as NodeInstance<string, string, unknown>;
|
||||
expect(() => wf.to(misuse)).toThrow(/Cannot pass an OutputSelector to \.to\(\)/);
|
||||
});
|
||||
|
||||
it('should throw a clear error when an OutputSelector is inside an array passed to .add()', () => {
|
||||
const t = trigger({ type: 'n8n-nodes-base.webhookTrigger', version: 1, config: {} });
|
||||
const source = node({
|
||||
type: 'n8n-nodes-base.if',
|
||||
version: 2,
|
||||
config: { name: 'Branch' },
|
||||
});
|
||||
const sibling = node({
|
||||
type: 'n8n-nodes-base.set',
|
||||
version: 3,
|
||||
config: { name: 'Sibling' },
|
||||
});
|
||||
|
||||
const wf = workflow('test-id', 'Test Workflow').add(t);
|
||||
const misuse = [sibling, source.output(0)] as unknown as NodeInstance<
|
||||
string,
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(() => wf.add(misuse)).toThrow(/Cannot pass an OutputSelector to \.add\(\)/);
|
||||
});
|
||||
|
||||
it('should accept node.output(n).to(target) inside .add() — the documented form', () => {
|
||||
const t = trigger({ type: 'n8n-nodes-base.webhookTrigger', version: 1, config: {} });
|
||||
const source = node({
|
||||
type: 'n8n-nodes-base.if',
|
||||
version: 2,
|
||||
config: { name: 'Branch' },
|
||||
});
|
||||
const target = node({
|
||||
type: 'n8n-nodes-base.set',
|
||||
version: 3,
|
||||
config: { name: 'Set' },
|
||||
});
|
||||
|
||||
const wf = workflow('test-id', 'Test Workflow').add(t).add(source.output(0).to(target));
|
||||
const json = wf.toJSON();
|
||||
const branchConns = json.connections[source.name]?.main[0];
|
||||
expect(branchConns).toBeDefined();
|
||||
expect(branchConns).toHaveLength(1);
|
||||
expect(branchConns?.[0]?.node).toBe(target.name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.to()', () => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,11 @@ import { isNodeChain } from './types/base';
|
|||
import type { ValidationOptions, ValidationResult, ValidationErrorCode } from './validation/index';
|
||||
import { ValidationError, ValidationWarning } from './validation/index';
|
||||
import { resolveTargetNodeName as resolveTargetNodeNameUtil } from './workflow-builder/connection-utils';
|
||||
import { isInputTarget, cloneNodeWithId } from './workflow-builder/node-builders/node-builder';
|
||||
import {
|
||||
isInputTarget,
|
||||
isOutputSelector,
|
||||
cloneNodeWithId,
|
||||
} from './workflow-builder/node-builders/node-builder';
|
||||
import { shouldGeneratePinData } from './workflow-builder/pin-data-utils';
|
||||
import { registerDefaultPlugins } from './workflow-builder/plugins/defaults';
|
||||
import { pluginRegistry, type PluginRegistry } from './workflow-builder/plugins/registry';
|
||||
|
|
@ -171,10 +175,13 @@ class WorkflowBuilderImpl implements WorkflowBuilder {
|
|||
}
|
||||
|
||||
add(node: unknown): WorkflowBuilder {
|
||||
assertNotOutputSelector(node, 'add');
|
||||
|
||||
// Handle plain array (fan-out)
|
||||
// This adds all targets without creating a primary connection
|
||||
if (Array.isArray(node)) {
|
||||
for (const target of node) {
|
||||
assertNotOutputSelector(target, 'add');
|
||||
if (isInputTarget(target)) {
|
||||
// InputTarget - add the target node
|
||||
const inputTargetNode = target.node;
|
||||
|
|
@ -263,6 +270,8 @@ class WorkflowBuilderImpl implements WorkflowBuilder {
|
|||
}
|
||||
|
||||
to(nodeOrComposite: unknown): WorkflowBuilder {
|
||||
assertNotOutputSelector(nodeOrComposite, 'to');
|
||||
|
||||
// Handle InputTarget (e.g., mergeNode.input(0))
|
||||
if (isInputTarget(nodeOrComposite)) {
|
||||
const actualNode = nodeOrComposite.node;
|
||||
|
|
@ -900,6 +909,8 @@ class WorkflowBuilderImpl implements WorkflowBuilder {
|
|||
return;
|
||||
}
|
||||
|
||||
assertNotOutputSelector(node, 'to');
|
||||
|
||||
// Use addBranchToGraph to handle NodeChains properly
|
||||
// This returns the head node name for connection
|
||||
const headNodeName = this.addBranchToGraph(
|
||||
|
|
@ -1162,6 +1173,18 @@ class WorkflowBuilderImpl implements WorkflowBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
function assertNotOutputSelector(value: unknown, method: 'add' | 'to'): void {
|
||||
if (!isOutputSelector(value)) return;
|
||||
const sourceName = value.node.name;
|
||||
throw new TypeError(
|
||||
`Cannot pass an OutputSelector to .${method}(). ` +
|
||||
`${sourceName}.output(${value.outputIndex}) by itself does not connect anything; ` +
|
||||
'chain `.to(target)` on the selector first to produce a connection. ' +
|
||||
`Example: .add(${sourceName}.output(${value.outputIndex}).to(targetNode)) ` +
|
||||
`— not .add(${sourceName}.output(${value.outputIndex})).to(targetNode).`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to check if options is a WorkflowBuilderOptions object
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user