refactor(core): Trim down NodeHelpers (no-changelog) (#14829)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2025-04-23 13:46:46 +02:00
committed by GitHub
parent 88bce7fd8b
commit 3e5e3a585c
11 changed files with 432 additions and 419 deletions

View File

@@ -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<IWorkflowExecuteAdditionalData>();
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;

View File

@@ -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([]);
});
});
});

View File

@@ -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',
},
],
};

View File

@@ -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;
}
}

View File

@@ -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 };
}
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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<typeof useNodeHelpers>['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);
});
});
});

View File

@@ -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,
};
}

View File

@@ -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
*/

View File

@@ -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<INodeTypeDescription, 'outputs'> | 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;