diff --git a/packages/core/src/execution-engine/__tests__/routing-node.test.ts b/packages/core/src/execution-engine/__tests__/routing-node.test.ts index a35ad24a8a..f2b7da53d8 100644 --- a/packages/core/src/execution-engine/__tests__/routing-node.test.ts +++ b/packages/core/src/execution-engine/__tests__/routing-node.test.ts @@ -19,9 +19,10 @@ import type { ITaskDataConnections, IWorkflowExecuteAdditionalData, } from 'n8n-workflow'; -import { NodeHelpers, Workflow } from 'n8n-workflow'; +import { Workflow } from 'n8n-workflow'; import * as executionContexts from '@/execution-engine/node-execution-context'; +import { DirectoryLoader } from '@/nodes-loader'; import { NodeTypes } from '@test/helpers'; import { RoutingNode } from '../routing-node'; @@ -87,23 +88,6 @@ describe('RoutingNode', () => { const nodeTypes = NodeTypes(); const additionalData = mock(); - test('applyDeclarativeNodeOptionParameters', () => { - const nodeTypes = NodeTypes(); - const nodeType = nodeTypes.getByNameAndVersion('test.setMulti'); - - NodeHelpers.applyDeclarativeNodeOptionParameters(nodeType); - - const options = nodeType.description.properties.find( - (property) => property.name === 'requestOptions', - ); - - expect(options?.options).toBeDefined; - - const optionNames = options!.options!.map((option) => option.name); - - expect(optionNames).toEqual(['batching', 'allowUnauthorizedCerts', 'proxy', 'timeout']); - }); - describe('getRequestOptionsFromParameters', () => { const tests: Array<{ description: string; @@ -1921,7 +1905,7 @@ describe('RoutingNode', () => { const connectionInputData: INodeExecutionData[] = []; const runExecutionData: IRunExecutionData = { resultData: { runData: {} } }; const nodeType = nodeTypes.getByNameAndVersion(baseNode.type); - NodeHelpers.applyDeclarativeNodeOptionParameters(nodeType); + DirectoryLoader.applyDeclarativeNodeOptionParameters(nodeType); const propertiesOriginal = nodeType.description.properties; diff --git a/packages/core/src/nodes-loader/__tests__/directory-loader.test.ts b/packages/core/src/nodes-loader/__tests__/directory-loader.test.ts index cb66300257..c577e9d398 100644 --- a/packages/core/src/nodes-loader/__tests__/directory-loader.test.ts +++ b/packages/core/src/nodes-loader/__tests__/directory-loader.test.ts @@ -22,7 +22,10 @@ jest.mock('fast-glob', () => async (pattern: string) => { : ['dist/Credential1.js']; }); +import { NodeTypes } from '@test/helpers'; + import { CustomDirectoryLoader } from '../custom-directory-loader'; +import { DirectoryLoader } from '../directory-loader'; import { LazyPackageDirectoryLoader } from '../lazy-package-directory-loader'; import * as classLoader from '../load-class-in-isolation'; import { PackageDirectoryLoader } from '../package-directory-loader'; @@ -755,4 +758,75 @@ describe('DirectoryLoader', () => { ); }); }); + + describe('applyDeclarativeNodeOptionParameters', () => { + test('sets up the options parameters', () => { + const nodeTypes = NodeTypes(); + const nodeType = nodeTypes.getByNameAndVersion('test.setMulti'); + + DirectoryLoader.applyDeclarativeNodeOptionParameters(nodeType); + + const options = nodeType.description.properties.find( + (property) => property.name === 'requestOptions', + ); + + expect(options?.options).toBeDefined; + + const optionNames = options!.options!.map((option) => option.name); + + expect(optionNames).toEqual(['batching', 'allowUnauthorizedCerts', 'proxy', 'timeout']); + }); + + 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; + DirectoryLoader.applyDeclarativeNodeOptionParameters(nodeType); + expect(nodeType.description.properties).toEqual([]); + }); + }); }); diff --git a/packages/core/src/nodes-loader/constants.ts b/packages/core/src/nodes-loader/constants.ts index 171be0352b..b7c577f28b 100644 --- a/packages/core/src/nodes-loader/constants.ts +++ b/packages/core/src/nodes-loader/constants.ts @@ -29,3 +29,84 @@ export const commonCORSParameters: INodeProperties[] = [ 'Comma-separated list of URLs allowed for cross-origin non-preflight requests. Use * (default) to allow all origins.', }, ]; + +export const commonDeclarativeNodeOptionParameters: INodeProperties = { + displayName: 'Request Options', + name: 'requestOptions', + type: 'collection', + isNodeSetting: true, + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Batching', + name: 'batching', + placeholder: 'Add Batching', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: { + batch: {}, + }, + options: [ + { + displayName: 'Batching', + name: 'batch', + values: [ + { + displayName: 'Items per Batch', + name: 'batchSize', + type: 'number', + typeOptions: { + minValue: -1, + }, + default: 50, + description: + 'Input will be split in batches to throttle requests. -1 for disabled. 0 will be treated as 1.', + }, + { + displayName: 'Batch Interval (ms)', + name: 'batchInterval', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 1000, + description: 'Time (in milliseconds) between each batch of requests. 0 for disabled.', + }, + ], + }, + ], + }, + { + displayName: 'Ignore SSL Issues (Insecure)', + name: 'allowUnauthorizedCerts', + type: 'boolean', + noDataExpression: true, + default: false, + description: + 'Whether to accept the response even if SSL certificate validation is not possible', + }, + { + displayName: 'Proxy', + name: 'proxy', + type: 'string', + default: '', + placeholder: 'e.g. http://myproxy:3128', + description: + 'HTTP proxy to use. If authentication is required it can be defined as follow: http://username:password@myproxy:3128', + }, + { + displayName: 'Timeout', + name: 'timeout', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 10000, + description: + 'Time in ms to wait for the server to send response headers (and start the response body) before aborting the request', + }, + ], +}; diff --git a/packages/core/src/nodes-loader/directory-loader.ts b/packages/core/src/nodes-loader/directory-loader.ts index 094aa8d1e0..cb67b47aea 100644 --- a/packages/core/src/nodes-loader/directory-loader.ts +++ b/packages/core/src/nodes-loader/directory-loader.ts @@ -15,14 +15,19 @@ import type { IVersionedNodeType, KnownNodesAndCredentials, } from 'n8n-workflow'; -import { ApplicationError, applyDeclarativeNodeOptionParameters } from 'n8n-workflow'; +import { ApplicationError, isSubNodeType } from 'n8n-workflow'; import * as path from 'path'; import { UnrecognizedCredentialTypeError } from '@/errors/unrecognized-credential-type.error'; import { UnrecognizedNodeTypeError } from '@/errors/unrecognized-node-type.error'; import { Logger } from '@/logging/logger'; -import { commonCORSParameters, commonPollingParameters, CUSTOM_NODES_CATEGORY } from './constants'; +import { + commonCORSParameters, + commonDeclarativeNodeOptionParameters, + commonPollingParameters, + CUSTOM_NODES_CATEGORY, +} from './constants'; import { loadClassInIsolation } from './load-class-in-isolation'; function toJSON(this: ICredentialType) { @@ -362,7 +367,7 @@ export abstract class DirectoryLoader { else properties.push(...commonCORSParameters); } - applyDeclarativeNodeOptionParameters(nodeType); + DirectoryLoader.applyDeclarativeNodeOptionParameters(nodeType); } private getIconPath(icon: string, filePath: string) { @@ -390,4 +395,59 @@ export abstract class DirectoryLoader { obj.icon = undefined; } } + + /** Augments additional `Request Options` property on declarative node-type */ + static applyDeclarativeNodeOptionParameters(nodeType: INodeType): void { + if ( + !!nodeType.execute || + !!nodeType.trigger || + !!nodeType.webhook || + !!nodeType.description.polling || + isSubNodeType(nodeType.description) + ) { + return; + } + + const parameters = nodeType.description.properties; + if (!parameters) { + return; + } + + // Was originally under "options" instead of "requestOptions" so the chance + // that that existed was quite high. With this name the chance is actually + // very low that it already exists but lets leave it in anyway to be sure. + const existingRequestOptionsIndex = parameters.findIndex( + (parameter) => parameter.name === 'requestOptions', + ); + if (existingRequestOptionsIndex !== -1) { + parameters[existingRequestOptionsIndex] = { + ...commonDeclarativeNodeOptionParameters, + options: [ + ...(commonDeclarativeNodeOptionParameters.options ?? []), + ...(parameters[existingRequestOptionsIndex]?.options ?? []), + ], + }; + + const options = parameters[existingRequestOptionsIndex]?.options; + + if (options) { + options.sort((a, b) => { + if ('displayName' in a && 'displayName' in b) { + if (a.displayName < b.displayName) { + return -1; + } + if (a.displayName > b.displayName) { + return 1; + } + } + + return 0; + }); + } + } else { + parameters.push(commonDeclarativeNodeOptionParameters); + } + + return; + } } diff --git a/packages/frontend/editor-ui/src/components/NDVSubConnections.vue b/packages/frontend/editor-ui/src/components/NDVSubConnections.vue index 52546a88f6..44666b61a6 100644 --- a/packages/frontend/editor-ui/src/components/NDVSubConnections.vue +++ b/packages/frontend/editor-ui/src/components/NDVSubConnections.vue @@ -106,7 +106,7 @@ function getINodesFromNames(names: string[]): NodeConfig[] { const matchedNodeType = nodeTypesStore.getNodeType(node.type); if (matchedNodeType) { const issues = nodeHelpers.getNodeIssues(matchedNodeType, node, workflow.value); - const stringifiedIssues = issues ? NodeHelpers.nodeIssuesToString(issues, node) : ''; + const stringifiedIssues = issues ? nodeHelpers.nodeIssuesToString(issues, node) : ''; return { node, nodeType: matchedNodeType, issues: stringifiedIssues }; } } diff --git a/packages/frontend/editor-ui/src/components/RunData.vue b/packages/frontend/editor-ui/src/components/RunData.vue index f2324f65e1..f171c6c696 100644 --- a/packages/frontend/editor-ui/src/components/RunData.vue +++ b/packages/frontend/editor-ui/src/components/RunData.vue @@ -746,7 +746,7 @@ function getNodeHints(): NodeHint[] { const workflowNode = props.workflow.getNode(node.value.name); if (workflowNode) { - const nodeHints = NodeHelpers.getNodeHints(props.workflow, workflowNode, nodeType.value, { + const nodeHints = nodeHelpers.getNodeHints(props.workflow, workflowNode, nodeType.value, { runExecutionData: workflowExecution.value?.data ?? null, runIndex: props.runIndex, connectionInputData: parentNodeOutputData.value, diff --git a/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts b/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts index ae92ec6c4d..b53fc373c4 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts @@ -381,7 +381,7 @@ export function useCanvasMapping({ } if (node?.issues !== undefined) { - issues.push(...NodeHelpers.nodeIssuesToString(node.issues, node)); + issues.push(...nodeHelpers.nodeIssuesToString(node.issues, node)); } acc[node.id] = issues; diff --git a/packages/frontend/editor-ui/src/composables/useNodeHelpers.test.ts b/packages/frontend/editor-ui/src/composables/useNodeHelpers.test.ts index c5ba6dc1f3..b051fad68c 100644 --- a/packages/frontend/editor-ui/src/composables/useNodeHelpers.test.ts +++ b/packages/frontend/editor-ui/src/composables/useNodeHelpers.test.ts @@ -1,4 +1,5 @@ import { setActivePinia } from 'pinia'; +import type { INode, INodeTypeDescription, Workflow } from 'n8n-workflow'; import { createTestingPinia } from '@pinia/testing'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { createTestNode } from '@/__tests__/mocks'; @@ -267,4 +268,98 @@ describe('useNodeHelpers()', () => { expect(isSingleExecution('n8n-nodes-base.redis', {})).toEqual(true); }); }); + + describe('getNodeHints', () => { + let getNodeHints: ReturnType['getNodeHints']; + beforeEach(() => { + getNodeHints = useNodeHelpers().getNodeHints; + }); + + //TODO: Add more tests here when hints are added to some node types + test('should return node hints if present in node type', () => { + const testType = { + hints: [ + { + message: 'TEST HINT', + }, + ], + } as INodeTypeDescription; + + const workflow = {} as unknown as Workflow; + + const node: INode = { + name: 'Test Node Hints', + } as INode; + const nodeType = testType; + + const hints = getNodeHints(workflow, node, nodeType); + + expect(hints).toHaveLength(1); + expect(hints[0].message).toEqual('TEST HINT'); + }); + test('should not include hint if displayCondition is false', () => { + const testType = { + hints: [ + { + message: 'TEST HINT', + displayCondition: 'FALSE DISPLAY CONDITION EXPESSION', + }, + ], + } as INodeTypeDescription; + + const workflow = { + expression: { + getSimpleParameterValue( + _node: string, + _parameter: string, + _mode: string, + _additionalData = {}, + ) { + return false; + }, + }, + } as unknown as Workflow; + + const node: INode = { + name: 'Test Node Hints', + } as INode; + const nodeType = testType; + + const hints = getNodeHints(workflow, node, nodeType); + + expect(hints).toHaveLength(0); + }); + test('should include hint if displayCondition is true', () => { + const testType = { + hints: [ + { + message: 'TEST HINT', + displayCondition: 'TRUE DISPLAY CONDITION EXPESSION', + }, + ], + } as INodeTypeDescription; + + const workflow = { + expression: { + getSimpleParameterValue( + _node: string, + _parameter: string, + _mode: string, + _additionalData = {}, + ) { + return true; + }, + }, + } as unknown as Workflow; + + const node: INode = { + name: 'Test Node Hints', + } as INode; + const nodeType = testType; + + const hints = getNodeHints(workflow, node, nodeType); + + expect(hints).toHaveLength(1); + }); + }); }); diff --git a/packages/frontend/editor-ui/src/composables/useNodeHelpers.ts b/packages/frontend/editor-ui/src/composables/useNodeHelpers.ts index ac93e8a72c..4c2f787134 100644 --- a/packages/frontend/editor-ui/src/composables/useNodeHelpers.ts +++ b/packages/frontend/editor-ui/src/composables/useNodeHelpers.ts @@ -28,6 +28,8 @@ import type { INodeTypeNameVersion, NodeParameterValue, NodeConnectionType, + IRunExecutionData, + NodeHint, } from 'n8n-workflow'; import type { @@ -885,6 +887,113 @@ export function useNodeHelpers() { return false; } + function getNodeHints( + workflow: Workflow, + node: INode, + nodeTypeData: INodeTypeDescription, + nodeInputData?: { + runExecutionData: IRunExecutionData | null; + runIndex: number; + connectionInputData: INodeExecutionData[]; + }, + ): NodeHint[] { + const hints: NodeHint[] = []; + + if (nodeTypeData?.hints?.length) { + for (const hint of nodeTypeData.hints) { + if (hint.displayCondition) { + try { + let display; + + if (nodeInputData === undefined) { + display = (workflow.expression.getSimpleParameterValue( + node, + hint.displayCondition, + 'internal', + {}, + ) || false) as boolean; + } else { + const { runExecutionData, runIndex, connectionInputData } = nodeInputData; + display = workflow.expression.getParameterValue( + hint.displayCondition, + runExecutionData ?? null, + runIndex, + 0, + node.name, + connectionInputData, + 'manual', + {}, + ); + } + + if (typeof display === 'string' && display.trim() === 'true') { + display = true; + } + + if (typeof display !== 'boolean') { + console.warn( + `Condition was not resolved as boolean in '${node.name}' node for hint: `, + hint.message, + ); + continue; + } + + if (display) { + hints.push(hint); + } + } catch (e) { + console.warn( + `Could not calculate display condition in '${node.name}' node for hint: `, + hint.message, + ); + } + } else { + hints.push(hint); + } + } + } + + return hints; + } + + /** + * Returns the issues of the node as string + * + * @param {INodeIssues} issues The issues of the node + * @param {INode} node The node + */ + function nodeIssuesToString(issues: INodeIssues, node?: INode): string[] { + const nodeIssues = []; + + if (issues.execution !== undefined) { + nodeIssues.push('Execution Error.'); + } + + const objectProperties = ['parameters', 'credentials', 'input']; + + let issueText: string; + let parameterName: string; + for (const propertyName of objectProperties) { + if (issues[propertyName] !== undefined) { + for (parameterName of Object.keys(issues[propertyName] as object)) { + for (issueText of (issues[propertyName] as INodeIssueObjectProperty)[parameterName]) { + nodeIssues.push(issueText); + } + } + } + } + + if (issues.typeUnknown !== undefined) { + if (node !== undefined) { + nodeIssues.push(`Node Type "${node.type}" is not known.`); + } else { + nodeIssues.push('Node Type is not known.'); + } + } + + return nodeIssues; + } + return { hasProxyAuth, isCustomApiCallSelected, @@ -914,5 +1023,7 @@ export function useNodeHelpers() { assignNodeId, assignWebhookId, isSingleExecution, + getNodeHints, + nodeIssuesToString, }; } diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index 770afd2aa6..eeb04ea5c8 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -34,8 +34,6 @@ import type { INodeInputConfiguration, GenericValue, DisplayCondition, - NodeHint, - INodeExecutionData, NodeConnectionType, } from './Interfaces'; import { validateFilterParameter } from './NodeParameters/FilterParameter'; @@ -235,87 +233,6 @@ export const cronNodeOptions: INodePropertyCollection[] = [ }, ]; -const declarativeNodeOptionParameters: INodeProperties = { - displayName: 'Request Options', - name: 'requestOptions', - type: 'collection', - isNodeSetting: true, - placeholder: 'Add Option', - default: {}, - options: [ - { - displayName: 'Batching', - name: 'batching', - placeholder: 'Add Batching', - type: 'fixedCollection', - typeOptions: { - multipleValues: false, - }, - default: { - batch: {}, - }, - options: [ - { - displayName: 'Batching', - name: 'batch', - values: [ - { - displayName: 'Items per Batch', - name: 'batchSize', - type: 'number', - typeOptions: { - minValue: -1, - }, - default: 50, - description: - 'Input will be split in batches to throttle requests. -1 for disabled. 0 will be treated as 1.', - }, - { - displayName: 'Batch Interval (ms)', - name: 'batchInterval', - type: 'number', - typeOptions: { - minValue: 0, - }, - default: 1000, - description: 'Time (in milliseconds) between each batch of requests. 0 for disabled.', - }, - ], - }, - ], - }, - { - displayName: 'Ignore SSL Issues (Insecure)', - name: 'allowUnauthorizedCerts', - type: 'boolean', - noDataExpression: true, - default: false, - description: - 'Whether to accept the response even if SSL certificate validation is not possible', - }, - { - displayName: 'Proxy', - name: 'proxy', - type: 'string', - default: '', - placeholder: 'e.g. http://myproxy:3128', - description: - 'HTTP proxy to use. If authentication is required it can be defined as follow: http://username:password@myproxy:3128', - }, - { - displayName: 'Timeout', - name: 'timeout', - type: 'number', - typeOptions: { - minValue: 1, - }, - default: 10000, - description: - 'Time in ms to wait for the server to send response headers (and start the response body) before aborting the request', - }, - ], -}; - /** * 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. @@ -332,62 +249,6 @@ export function isSubNodeType( : 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 || - isSubNodeType(nodeType.description) - ) { - return; - } - - const parameters = nodeType.description.properties; - - if (!parameters) { - return; - } - - // Was originally under "options" instead of "requestOptions" so the chance - // that that existed was quite high. With this name the chance is actually - // very low that it already exists but lets leave it in anyway to be sure. - const existingRequestOptionsIndex = parameters.findIndex( - (parameter) => parameter.name === 'requestOptions', - ); - if (existingRequestOptionsIndex !== -1) { - parameters[existingRequestOptionsIndex] = { - ...declarativeNodeOptionParameters, - options: [ - ...(declarativeNodeOptionParameters.options || []), - ...(parameters[existingRequestOptionsIndex]?.options || []), - ], - }; - - const options = parameters[existingRequestOptionsIndex]?.options; - - if (options) { - options.sort((a, b) => { - if ('displayName' in a && 'displayName' in b) { - if (a.displayName < b.displayName) { - return -1; - } - if (a.displayName > b.displayName) { - return 1; - } - } - - return 0; - }); - } - } else { - parameters.push(declarativeNodeOptionParameters); - } - - return; -} - const getPropertyValues = ( nodeValues: INodeParameters, propertyName: string, @@ -660,7 +521,7 @@ function getParameterDependencies(nodePropertiesArray: INodeProperties[]): IPara * Returns in which order the parameters should be resolved * to have the parameters available they depend on */ -export function getParameterResolveOrder( +function getParameterResolveOrder( nodePropertiesArray: INodeProperties[], parameterDependencies: IParameterDependencies, ): number[] { @@ -722,7 +583,7 @@ export function getParameterResolveOrder( return executionOrder; } -export type GetNodeParametersOptions = { +type GetNodeParametersOptions = { onlySimpleTypes?: boolean; dataIsResolved?: boolean; // If nodeValues are already fully resolved (so that all default values got added already) nodeValuesRoot?: INodeParameters; @@ -1164,75 +1025,6 @@ export function getNodeInputs( } } -export function getNodeHints( - workflow: Workflow, - node: INode, - nodeTypeData: INodeTypeDescription, - nodeInputData?: { - runExecutionData: IRunExecutionData | null; - runIndex: number; - connectionInputData: INodeExecutionData[]; - }, -): NodeHint[] { - const hints: NodeHint[] = []; - - if (nodeTypeData?.hints?.length) { - for (const hint of nodeTypeData.hints) { - if (hint.displayCondition) { - try { - let display; - - if (nodeInputData === undefined) { - display = (workflow.expression.getSimpleParameterValue( - node, - hint.displayCondition, - 'internal', - {}, - ) || false) as boolean; - } else { - const { runExecutionData, runIndex, connectionInputData } = nodeInputData; - display = workflow.expression.getParameterValue( - hint.displayCondition, - runExecutionData ?? null, - runIndex, - 0, - node.name, - connectionInputData, - 'manual', - {}, - ); - } - - if (typeof display === 'string' && display.trim() === 'true') { - display = true; - } - - if (typeof display !== 'boolean') { - console.warn( - `Condition was not resolved as boolean in '${node.name}' node for hint: `, - hint.message, - ); - continue; - } - - if (display) { - hints.push(hint); - } - } catch (e) { - console.warn( - `Could not calculate display condition in '${node.name}' node for hint: `, - hint.message, - ); - } - } else { - hints.push(hint); - } - } - } - - return hints; -} - export function getNodeOutputs( workflow: Workflow, node: INode, @@ -1320,44 +1112,6 @@ export function getNodeParametersIssues( return foundIssues; } -/** - * Returns the issues of the node as string - * - * @param {INodeIssues} issues The issues of the node - * @param {INode} node The node - */ -export function nodeIssuesToString(issues: INodeIssues, node?: INode): string[] { - const nodeIssues = []; - - if (issues.execution !== undefined) { - nodeIssues.push('Execution Error.'); - } - - const objectProperties = ['parameters', 'credentials', 'input']; - - let issueText: string; - let parameterName: string; - for (const propertyName of objectProperties) { - if (issues[propertyName] !== undefined) { - for (parameterName of Object.keys(issues[propertyName] as object)) { - for (issueText of (issues[propertyName] as INodeIssueObjectProperty)[parameterName]) { - nodeIssues.push(issueText); - } - } - } - } - - if (issues.typeUnknown !== undefined) { - if (node !== undefined) { - nodeIssues.push(`Node Type "${node.type}" is not known.`); - } else { - nodeIssues.push('Node Type is not known.'); - } - } - - return nodeIssues; -} - /* * Validates resource locator node parameters based on validation ruled defined in each parameter mode */ diff --git a/packages/workflow/test/NodeHelpers.test.ts b/packages/workflow/test/NodeHelpers.test.ts index 9716f5b1fc..6aed38da50 100644 --- a/packages/workflow/test/NodeHelpers.test.ts +++ b/packages/workflow/test/NodeHelpers.test.ts @@ -5,14 +5,11 @@ import { type INode, type INodeParameters, type INodeProperties, - type INodeType, type INodeTypeDescription, } from '@/Interfaces'; import { getNodeParameters, - getNodeHints, isSubNodeType, - applyDeclarativeNodeOptionParameters, getParameterIssues, isTriggerNode, isExecutable, @@ -3461,95 +3458,6 @@ describe('NodeHelpers', () => { } }); - describe('getNodeHints', () => { - //TODO: Add more tests here when hints are added to some node types - test('should return node hints if present in node type', () => { - const testType = { - hints: [ - { - message: 'TEST HINT', - }, - ], - } as INodeTypeDescription; - - const workflow = {} as unknown as Workflow; - - const node: INode = { - name: 'Test Node Hints', - } as INode; - const nodeType = testType; - - const hints = getNodeHints(workflow, node, nodeType); - - expect(hints).toHaveLength(1); - expect(hints[0].message).toEqual('TEST HINT'); - }); - test('should not include hint if displayCondition is false', () => { - const testType = { - hints: [ - { - message: 'TEST HINT', - displayCondition: 'FALSE DISPLAY CONDITION EXPESSION', - }, - ], - } as INodeTypeDescription; - - const workflow = { - expression: { - getSimpleParameterValue( - _node: string, - _parameter: string, - _mode: string, - _additionalData = {}, - ) { - return false; - }, - }, - } as unknown as Workflow; - - const node: INode = { - name: 'Test Node Hints', - } as INode; - const nodeType = testType; - - const hints = getNodeHints(workflow, node, nodeType); - - expect(hints).toHaveLength(0); - }); - test('should include hint if displayCondition is true', () => { - const testType = { - hints: [ - { - message: 'TEST HINT', - displayCondition: 'TRUE DISPLAY CONDITION EXPESSION', - }, - ], - } as INodeTypeDescription; - - const workflow = { - expression: { - getSimpleParameterValue( - _node: string, - _parameter: string, - _mode: string, - _additionalData = {}, - ) { - return true; - }, - }, - } as unknown as Workflow; - - const node: INode = { - name: 'Test Node Hints', - } as INode; - const nodeType = testType; - - const hints = getNodeHints(workflow, node, nodeType); - - expect(hints).toHaveLength(1); - }); - }); - describe('isSubNodeType', () => { const tests: Array<[boolean, Pick | null]> = [ [false, null], @@ -3564,60 +3472,6 @@ describe('NodeHelpers', () => { }); }); - 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([]); - }); - }); - describe('getParameterIssues', () => { const tests: Array<{ description: string;