diff --git a/packages/editor-ui/src/composables/__tests__/useNodeType.test.ts b/packages/editor-ui/src/composables/__tests__/useNodeType.test.ts index 1cd25c7864..b0ec26fbf0 100644 --- a/packages/editor-ui/src/composables/__tests__/useNodeType.test.ts +++ b/packages/editor-ui/src/composables/__tests__/useNodeType.test.ts @@ -25,18 +25,6 @@ describe('useNodeType()', () => { }); }); - describe('isSubNodeType', () => { - it('identifies sub node type correctly', () => { - const nodeTypeOption = { - name: 'testNodeType', - outputs: ['Main', 'Other'], - } as unknown as SimplifiedNodeType; - const { isSubNodeType } = useNodeType({ nodeType: nodeTypeOption }); - - expect(isSubNodeType.value).toBe(true); - }); - }); - describe('isMultipleOutputsNodeType', () => { it('identifies multiple outputs node type correctly', () => { const nodeTypeOption = { diff --git a/packages/editor-ui/src/composables/useNodeType.ts b/packages/editor-ui/src/composables/useNodeType.ts index d41c22c001..4a78f84c68 100644 --- a/packages/editor-ui/src/composables/useNodeType.ts +++ b/packages/editor-ui/src/composables/useNodeType.ts @@ -3,7 +3,7 @@ import { computed, unref } from 'vue'; import type { INodeTypeDescription } from 'n8n-workflow'; import type { INodeUi, SimplifiedNodeType } from '@/Interface'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; -import { NodeConnectionType, NodeHelpers } from 'n8n-workflow'; +import { NodeHelpers } from 'n8n-workflow'; export function useNodeType( options: { @@ -26,15 +26,7 @@ export function useNodeType( return null; }); - const isSubNodeType = computed(() => { - if (!nodeType.value?.outputs || typeof nodeType.value?.outputs === 'string') { - return false; - } - const outputTypes = NodeHelpers.getConnectionTypes(nodeType.value?.outputs); - return outputTypes - ? outputTypes.filter((output) => output !== NodeConnectionType.Main).length > 0 - : false; - }); + const isSubNodeType = computed(() => NodeHelpers.isSubNodeType(nodeType.value)); const isMultipleOutputsNodeType = computed(() => { const outputs = nodeType.value?.outputs; diff --git a/packages/editor-ui/src/utils/__tests__/canvasUtilsV2.spec.ts b/packages/editor-ui/src/utils/__tests__/canvasUtilsV2.spec.ts index 3e3074def7..85285745cd 100644 --- a/packages/editor-ui/src/utils/__tests__/canvasUtilsV2.spec.ts +++ b/packages/editor-ui/src/utils/__tests__/canvasUtilsV2.spec.ts @@ -530,13 +530,13 @@ describe('mapCanvasConnectionToLegacyConnection', () => { describe('mapLegacyEndpointsToCanvasConnectionPort', () => { it('should return an empty array and log a warning when inputs is a string', () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const endpoints: INodeTypeDescription['inputs'] = 'some code'; + const endpoints: INodeTypeDescription['inputs'] = '={{some code}}'; const result = mapLegacyEndpointsToCanvasConnectionPort(endpoints); expect(result).toEqual([]); expect(consoleWarnSpy).toHaveBeenCalledWith( 'Node endpoints have not been evaluated', - 'some code', + '={{some code}}', ); consoleWarnSpy.mockRestore(); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 172dae846c..d8d824db8f 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1766,21 +1766,24 @@ export interface INodeInputConfiguration { } export interface INodeOutputConfiguration { - category?: string; + category?: 'error'; displayName?: string; + maxConnections?: number; required?: boolean; type: ConnectionTypes; } +export type ExpressionString = `={{${string}}}`; + export interface INodeTypeDescription extends INodeTypeBaseDescription { version: number | number[]; defaults: INodeParameters; eventTriggerDescription?: string; activationMessage?: string; - inputs: Array | string; + inputs: Array | ExpressionString; requiredInputs?: string | number[] | number; // Ony available with executionOrder => "v1" inputNames?: string[]; - outputs: Array | string; + outputs: Array | ExpressionString; outputNames?: string[]; properties: INodeProperties[]; credentials?: INodeCredentialDescription[]; diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index 13e81772bd..b60aa3b1c2 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -10,6 +10,7 @@ import get from 'lodash/get'; import isEqual from 'lodash/isEqual'; import uniqBy from 'lodash/uniqBy'; +import { NodeConnectionType } from './Interfaces'; import type { FieldType, IContextObject, @@ -351,8 +352,31 @@ const declarativeNodeOptionParameters: INodeProperties = { ], }; +/** + * Determines if the provided node type has any output types other than the main connection type. + * @param typeDescription The node's type description to check. + */ +export function isSubNodeType( + typeDescription: Pick | null, +): boolean { + if (!typeDescription || !typeDescription.outputs || typeof typeDescription.outputs === 'string') { + return false; + } + const outputTypes = getConnectionTypes(typeDescription.outputs); + return outputTypes + ? outputTypes.filter((output) => output !== NodeConnectionType.Main).length > 0 + : false; +} + +/** Augments additional `Request Options` property on declarative node-type */ export function applyDeclarativeNodeOptionParameters(nodeType: INodeType): void { - if (nodeType.execute || nodeType.trigger || nodeType.webhook || nodeType.description.polling) { + if ( + nodeType.execute || + nodeType.trigger || + nodeType.webhook || + nodeType.description.polling || + isSubNodeType(nodeType.description) + ) { return; } diff --git a/packages/workflow/test/NodeHelpers.test.ts b/packages/workflow/test/NodeHelpers.test.ts index 74508cdce9..142bc82a20 100644 --- a/packages/workflow/test/NodeHelpers.test.ts +++ b/packages/workflow/test/NodeHelpers.test.ts @@ -1,7 +1,18 @@ -import type { INode, INodeParameters, INodeProperties, INodeTypeDescription } from '@/Interfaces'; +import type { + INode, + INodeParameters, + INodeProperties, + INodeType, + INodeTypeDescription, +} from '@/Interfaces'; import type { Workflow } from '@/Workflow'; - -import { getNodeParameters, getNodeHints, isSingleExecution } from '@/NodeHelpers'; +import { + getNodeParameters, + getNodeHints, + isSingleExecution, + isSubNodeType, + applyDeclarativeNodeOptionParameters, +} from '@/NodeHelpers'; describe('NodeHelpers', () => { describe('getNodeParameters', () => { @@ -3528,6 +3539,7 @@ describe('NodeHelpers', () => { expect(hints).toHaveLength(1); }); }); + describe('isSingleExecution', () => { test('should determine based on node parameters if it would be executed once', () => { expect(isSingleExecution('n8n-nodes-base.code', {})).toEqual(true); @@ -3555,4 +3567,72 @@ describe('NodeHelpers', () => { expect(isSingleExecution('n8n-nodes-base.redis', {})).toEqual(true); }); }); + + describe('isSubNodeType', () => { + const tests: Array<[boolean, Pick | null]> = [ + [false, null], + [false, { outputs: '={{random_expression}}' }], + [false, { outputs: [] }], + [false, { outputs: ['main'] }], + [true, { outputs: ['ai_agent'] }], + [true, { outputs: ['main', 'ai_agent'] }], + ]; + test.each(tests)('should return %p for %o', (expected, nodeType) => { + expect(isSubNodeType(nodeType)).toBe(expected); + }); + }); + + describe('applyDeclarativeNodeOptionParameters', () => { + test.each([ + [ + 'node with execute method', + { + execute: jest.fn(), + description: { + properties: [], + }, + }, + ], + [ + 'node with trigger method', + { + trigger: jest.fn(), + description: { + properties: [], + }, + }, + ], + [ + 'node with webhook method', + { + webhook: jest.fn(), + description: { + properties: [], + }, + }, + ], + [ + 'a polling node-type', + { + description: { + polling: true, + properties: [], + }, + }, + ], + [ + 'a node-type with a non-main output', + { + description: { + outputs: ['main', 'ai_agent'], + properties: [], + }, + }, + ], + ])('should not modify properties on node with %s method', (_, nodeTypeName) => { + const nodeType = nodeTypeName as unknown as INodeType; + applyDeclarativeNodeOptionParameters(nodeType); + expect(nodeType.description.properties).toEqual([]); + }); + }); });