mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-21 04:37:50 +08:00
Merge branch 'feat/workflow-sdk-fetch-1000-workflows' of github.com:n8n-io/n8n into feat/fix-more-issues
This commit is contained in:
@@ -889,4 +889,235 @@ export default workflow('test', 'Test').add(t.to(n));`;
|
||||
expect(parsedJson.connections['Process']?.main[0]?.[0]?.node).toBe('Process 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Case: onError with IF composite', () => {
|
||||
it('should roundtrip onError connected to IF with true/false branches', () => {
|
||||
const originalJson: WorkflowJSON = {
|
||||
id: 'onerror-if-test',
|
||||
name: 'OnError IF Test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'trigger-1',
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'http-1',
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [200, 0],
|
||||
parameters: { url: 'https://api.example.com' },
|
||||
onError: 'continueErrorOutput',
|
||||
},
|
||||
{
|
||||
id: 'if-1',
|
||||
name: 'Error IF',
|
||||
type: 'n8n-nodes-base.if',
|
||||
typeVersion: 2,
|
||||
position: [400, 100],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'true-1',
|
||||
name: 'Retry',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.4,
|
||||
position: [600, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'false-1',
|
||||
name: 'Log Error',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.4,
|
||||
position: [600, 200],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Start: {
|
||||
main: [[{ node: 'HTTP Request', type: 'main', index: 0 }]],
|
||||
},
|
||||
'HTTP Request': {
|
||||
main: [
|
||||
[], // Output 0 - success (empty)
|
||||
[{ node: 'Error IF', type: 'main', index: 0 }], // Output 1 - error
|
||||
],
|
||||
},
|
||||
'Error IF': {
|
||||
main: [
|
||||
[{ node: 'Retry', type: 'main', index: 0 }],
|
||||
[{ node: 'Log Error', type: 'main', index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const code = generateWorkflowCode(originalJson);
|
||||
const parsedJson = parseWorkflowCode(code);
|
||||
|
||||
expect(parsedJson.nodes).toHaveLength(5);
|
||||
expect(parsedJson.connections['HTTP Request']?.main[1]?.[0]?.node).toBe('Error IF');
|
||||
expect(parsedJson.connections['Error IF']?.main[0]?.[0]?.node).toBe('Retry');
|
||||
expect(parsedJson.connections['Error IF']?.main[1]?.[0]?.node).toBe('Log Error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Case: onError with Switch composite', () => {
|
||||
it('should roundtrip onError connected to Switch with case branches', () => {
|
||||
const originalJson: WorkflowJSON = {
|
||||
id: 'onerror-switch-test',
|
||||
name: 'OnError Switch Test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'trigger-1',
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'http-1',
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [200, 0],
|
||||
parameters: { url: 'https://api.example.com' },
|
||||
onError: 'continueErrorOutput',
|
||||
},
|
||||
{
|
||||
id: 'switch-1',
|
||||
name: 'Error Router',
|
||||
type: 'n8n-nodes-base.switch',
|
||||
typeVersion: 3,
|
||||
position: [400, 100],
|
||||
parameters: { mode: 'rules' },
|
||||
},
|
||||
{
|
||||
id: 'case-0',
|
||||
name: 'Handle 404',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.4,
|
||||
position: [600, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'case-1',
|
||||
name: 'Handle 500',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.4,
|
||||
position: [600, 200],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Start: {
|
||||
main: [[{ node: 'HTTP Request', type: 'main', index: 0 }]],
|
||||
},
|
||||
'HTTP Request': {
|
||||
main: [
|
||||
[], // Output 0 - success (empty)
|
||||
[{ node: 'Error Router', type: 'main', index: 0 }], // Output 1 - error
|
||||
],
|
||||
},
|
||||
'Error Router': {
|
||||
main: [
|
||||
[{ node: 'Handle 404', type: 'main', index: 0 }],
|
||||
[{ node: 'Handle 500', type: 'main', index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const code = generateWorkflowCode(originalJson);
|
||||
const parsedJson = parseWorkflowCode(code);
|
||||
|
||||
expect(parsedJson.nodes).toHaveLength(5);
|
||||
expect(parsedJson.connections['HTTP Request']?.main[1]?.[0]?.node).toBe('Error Router');
|
||||
expect(parsedJson.connections['Error Router']?.main[0]?.[0]?.node).toBe('Handle 404');
|
||||
expect(parsedJson.connections['Error Router']?.main[1]?.[0]?.node).toBe('Handle 500');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Case: onError with SplitInBatches composite', () => {
|
||||
it('should roundtrip onError connected to SplitInBatches with done/each branches', () => {
|
||||
const originalJson: WorkflowJSON = {
|
||||
id: 'onerror-sib-test',
|
||||
name: 'OnError SIB Test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'trigger-1',
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'http-1',
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 4.2,
|
||||
position: [200, 0],
|
||||
parameters: { url: 'https://api.example.com' },
|
||||
onError: 'continueErrorOutput',
|
||||
},
|
||||
{
|
||||
id: 'sib-1',
|
||||
name: 'Error Batcher',
|
||||
type: 'n8n-nodes-base.splitInBatches',
|
||||
typeVersion: 3,
|
||||
position: [400, 100],
|
||||
parameters: { batchSize: 10 },
|
||||
},
|
||||
{
|
||||
id: 'done-1',
|
||||
name: 'All Done',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.4,
|
||||
position: [600, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'each-1',
|
||||
name: 'Process Each',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.4,
|
||||
position: [600, 200],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Start: {
|
||||
main: [[{ node: 'HTTP Request', type: 'main', index: 0 }]],
|
||||
},
|
||||
'HTTP Request': {
|
||||
main: [
|
||||
[], // Output 0 - success (empty)
|
||||
[{ node: 'Error Batcher', type: 'main', index: 0 }], // Output 1 - error
|
||||
],
|
||||
},
|
||||
'Error Batcher': {
|
||||
main: [
|
||||
[{ node: 'All Done', type: 'main', index: 0 }], // Output 0 - done
|
||||
[{ node: 'Process Each', type: 'main', index: 0 }], // Output 1 - each
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const code = generateWorkflowCode(originalJson);
|
||||
const parsedJson = parseWorkflowCode(code);
|
||||
|
||||
expect(parsedJson.nodes).toHaveLength(5);
|
||||
expect(parsedJson.connections['HTTP Request']?.main[1]?.[0]?.node).toBe('Error Batcher');
|
||||
expect(parsedJson.connections['Error Batcher']?.main[0]?.[0]?.node).toBe('All Done');
|
||||
expect(parsedJson.connections['Error Batcher']?.main[1]?.[0]?.node).toBe('Process Each');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { NodeInstance, WorkflowJSON } from './types/base';
|
||||
import { workflow } from './workflow-builder';
|
||||
import { splitInBatches } from './workflow-builder/control-flow-builders/split-in-batches';
|
||||
import { node, trigger, sticky } from './workflow-builder/node-builders/node-builder';
|
||||
import {
|
||||
languageModel,
|
||||
@@ -415,6 +416,7 @@ describe('Workflow Builder', () => {
|
||||
expect(json.connections['Send Slack']?.error?.[0]?.[0]?.node).toBe('Error Alert');
|
||||
});
|
||||
|
||||
|
||||
it('should add nodes from nested .onError() chains', () => {
|
||||
// Build: trigger → http1.onError(http2.onError(errorFinal.to(downstream)))
|
||||
// All 5 nodes should appear in toJSON().nodes
|
||||
@@ -467,6 +469,188 @@ describe('Workflow Builder', () => {
|
||||
// errorFinal → downstream
|
||||
expect(json.connections['Error Final']?.main[0]?.[0]?.node).toBe('Downstream');
|
||||
});
|
||||
|
||||
it('should handle IfElse composite as onError target on standalone node', () => {
|
||||
const httpNode = node({
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
version: 4.2,
|
||||
config: { name: 'HTTP', onError: 'continueErrorOutput' },
|
||||
});
|
||||
const ifNode = node({
|
||||
type: 'n8n-nodes-base.if',
|
||||
version: 2,
|
||||
config: { name: 'IF' },
|
||||
}) as NodeInstance<'n8n-nodes-base.if', string, unknown>;
|
||||
const trueHandler = node({
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
version: 1,
|
||||
config: { name: 'True Handler' },
|
||||
});
|
||||
const falseHandler = node({
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
version: 1,
|
||||
config: { name: 'False Handler' },
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
httpNode.onError(ifNode.onTrue!(trueHandler).onFalse(falseHandler) as any);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const wf = workflow('test-id', 'Test').add(httpNode as any);
|
||||
const json = wf.toJSON();
|
||||
|
||||
expect(json.nodes).toHaveLength(4);
|
||||
expect(json.connections['HTTP']?.main[1]?.[0]?.node).toBe('IF');
|
||||
expect(json.connections['IF']?.main[0]?.[0]?.node).toBe('True Handler');
|
||||
expect(json.connections['IF']?.main[1]?.[0]?.node).toBe('False Handler');
|
||||
});
|
||||
|
||||
it('should handle SwitchCase composite as onError target on standalone node', () => {
|
||||
const httpNode = node({
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
version: 4.2,
|
||||
config: { name: 'HTTP', onError: 'continueErrorOutput' },
|
||||
});
|
||||
const switchNode = node({
|
||||
type: 'n8n-nodes-base.switch',
|
||||
version: 3.2,
|
||||
config: { name: 'Switch', parameters: { mode: 'rules' } },
|
||||
}) as NodeInstance<'n8n-nodes-base.switch', string, unknown>;
|
||||
const case0 = node({
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
version: 1,
|
||||
config: { name: 'Case 0' },
|
||||
});
|
||||
const case1 = node({
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
version: 1,
|
||||
config: { name: 'Case 1' },
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
httpNode.onError(switchNode.onCase!(0, case0).onCase(1, case1) as any);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const wf = workflow('test-id', 'Test').add(httpNode as any);
|
||||
const json = wf.toJSON();
|
||||
|
||||
expect(json.nodes).toHaveLength(4);
|
||||
expect(json.connections['HTTP']?.main[1]?.[0]?.node).toBe('Switch');
|
||||
expect(json.connections['Switch']?.main[0]?.[0]?.node).toBe('Case 0');
|
||||
expect(json.connections['Switch']?.main[1]?.[0]?.node).toBe('Case 1');
|
||||
});
|
||||
|
||||
it('should handle SplitInBatches composite as onError target on standalone node', () => {
|
||||
const httpNode = node({
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
version: 4.2,
|
||||
config: { name: 'HTTP', onError: 'continueErrorOutput' },
|
||||
});
|
||||
const sibNode = node({
|
||||
type: 'n8n-nodes-base.splitInBatches',
|
||||
version: 3,
|
||||
config: { name: 'SIB', parameters: { batchSize: 10 } },
|
||||
});
|
||||
const doneNode = node({
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
version: 1,
|
||||
config: { name: 'Done' },
|
||||
});
|
||||
const eachNode = node({
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
version: 1,
|
||||
config: { name: 'Each' },
|
||||
});
|
||||
|
||||
const sibBuilder = splitInBatches(sibNode).onDone(doneNode).onEachBatch(eachNode);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
httpNode.onError(sibBuilder as any);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const wf = workflow('test-id', 'Test').add(httpNode as any);
|
||||
const json = wf.toJSON();
|
||||
|
||||
expect(json.nodes).toHaveLength(4);
|
||||
expect(json.connections['HTTP']?.main[1]?.[0]?.node).toBe('SIB');
|
||||
expect(json.connections['SIB']?.main[0]?.[0]?.node).toBe('Done');
|
||||
expect(json.connections['SIB']?.main[1]?.[0]?.node).toBe('Each');
|
||||
});
|
||||
|
||||
it('should handle IfElse composite as onError target in chain', () => {
|
||||
const t = trigger({
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
version: 1,
|
||||
config: { name: 'Start' },
|
||||
});
|
||||
const httpNode = node({
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
version: 4.2,
|
||||
config: { name: 'HTTP', onError: 'continueErrorOutput' },
|
||||
});
|
||||
const ifNode = node({
|
||||
type: 'n8n-nodes-base.if',
|
||||
version: 2,
|
||||
config: { name: 'IF' },
|
||||
}) as NodeInstance<'n8n-nodes-base.if', string, unknown>;
|
||||
const trueHandler = node({
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
version: 1,
|
||||
config: { name: 'True Handler' },
|
||||
});
|
||||
const falseHandler = node({
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
version: 1,
|
||||
config: { name: 'False Handler' },
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
httpNode.onError(ifNode.onTrue!(trueHandler).onFalse(falseHandler) as any);
|
||||
const wf = workflow('test-id', 'Test').add(t).to(httpNode);
|
||||
const json = wf.toJSON();
|
||||
|
||||
expect(json.nodes).toHaveLength(5);
|
||||
expect(json.connections['Start']?.main[0]?.[0]?.node).toBe('HTTP');
|
||||
expect(json.connections['HTTP']?.main[1]?.[0]?.node).toBe('IF');
|
||||
expect(json.connections['IF']?.main[0]?.[0]?.node).toBe('True Handler');
|
||||
expect(json.connections['IF']?.main[1]?.[0]?.node).toBe('False Handler');
|
||||
});
|
||||
|
||||
it('should handle chain leading to composite via onError', () => {
|
||||
const httpNode = node({
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
version: 4.2,
|
||||
config: { name: 'HTTP', onError: 'continueErrorOutput' },
|
||||
});
|
||||
const logNode = node({
|
||||
type: 'n8n-nodes-base.set',
|
||||
version: 3.4,
|
||||
config: { name: 'Log' },
|
||||
});
|
||||
const ifNode = node({
|
||||
type: 'n8n-nodes-base.if',
|
||||
version: 2,
|
||||
config: { name: 'IF' },
|
||||
}) as NodeInstance<'n8n-nodes-base.if', string, unknown>;
|
||||
const trueHandler = node({
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
version: 1,
|
||||
config: { name: 'True Handler' },
|
||||
});
|
||||
const falseHandler = node({
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
version: 1,
|
||||
config: { name: 'False Handler' },
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
httpNode.onError(logNode.to(ifNode.onTrue!(trueHandler).onFalse(falseHandler) as any));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const wf = workflow('test-id', 'Test').add(httpNode as any);
|
||||
const json = wf.toJSON();
|
||||
|
||||
expect(json.nodes).toHaveLength(5);
|
||||
expect(json.connections['HTTP']?.main[1]?.[0]?.node).toBe('Log');
|
||||
expect(json.connections['Log']?.main[0]?.[0]?.node).toBe('IF');
|
||||
expect(json.connections['IF']?.main[0]?.[0]?.node).toBe('True Handler');
|
||||
expect(json.connections['IF']?.main[1]?.[0]?.node).toBe('False Handler');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.settings()', () => {
|
||||
|
||||
@@ -1089,6 +1089,13 @@ class WorkflowBuilderImpl implements WorkflowBuilder {
|
||||
this.addSingleNodeConnectionTargets(nodes, targetChainNode);
|
||||
}
|
||||
}
|
||||
} else if (registry.isCompositeType(target)) {
|
||||
// Only dispatch if the composite's head node isn't already in the graph
|
||||
// (avoids re-dispatching composites already handled by the allNodes loop above)
|
||||
const compositeHeadName = this.resolveTargetNodeName(target, effectiveNameMapping);
|
||||
if (!compositeHeadName || !nodes.has(compositeHeadName)) {
|
||||
this.tryPluginDispatch(nodes, target, effectiveNameMapping);
|
||||
}
|
||||
} else if (
|
||||
typeof (target as NodeInstance<string, string, unknown>).name === 'string' &&
|
||||
!nodes.has((target as NodeInstance<string, string, unknown>).name)
|
||||
|
||||
@@ -160,7 +160,9 @@ const handleClick = (event: MouseEvent) => {
|
||||
|
||||
background-color: var(--button--color--background);
|
||||
color: var(--button--color);
|
||||
box-shadow: var(--button--shadow), var(--button--border--shadow);
|
||||
box-shadow:
|
||||
inset var(--button--border--shadow),
|
||||
var(--button--shadow);
|
||||
border: none;
|
||||
|
||||
> * {
|
||||
@@ -169,12 +171,16 @@ const handleClick = (event: MouseEvent) => {
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button--color--background-hover);
|
||||
box-shadow: var(--button--shadow--hover), var(--button--border--shadow--hover);
|
||||
box-shadow:
|
||||
inset var(--button--border--shadow--hover),
|
||||
var(--button--shadow--hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--button--color--background-active);
|
||||
box-shadow: var(--button--shadow--active), var(--button--border--shadow--active);
|
||||
box-shadow:
|
||||
inset var(--button--border--shadow--active),
|
||||
var(--button--shadow--active);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
|
||||
Reference in New Issue
Block a user