mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
refactor(core): Trim down NodeHelpers (no-changelog) (#14829)
This commit is contained in:
committed by
GitHub
parent
88bce7fd8b
commit
3e5e3a585c
@@ -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;
|
||||
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user