feat: (Execute Workflow Node): Inputs for Sub-workflows (#11830) (#11837)

Co-authored-by: Charlie Kolb <charlie@n8n.io>
Co-authored-by: Milorad FIlipović <milorad@n8n.io>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Ivan Atanasov
2024-12-20 17:01:22 +01:00
committed by GitHub
parent 6c323e4e49
commit d4116630a6
52 changed files with 4023 additions and 688 deletions

View File

@@ -1017,9 +1017,23 @@ export interface ILoadOptionsFunctions extends FunctionsBase {
options?: IGetNodeParameterOptions,
): NodeParameterValueType | object | undefined;
getCurrentNodeParameters(): INodeParameters | undefined;
helpers: RequestHelperFunctions & SSHTunnelFunctions;
}
export type FieldValueOption = { name: string; type: FieldType | 'any' };
export type IWorkflowNodeContext = ExecuteFunctions.GetNodeParameterFn &
Pick<FunctionsBase, 'getNode'>;
export interface ILocalLoadOptionsFunctions {
getWorkflowNodeContext(nodeType: string): Promise<IWorkflowNodeContext | null>;
}
export interface IWorkflowLoader {
get(workflowId: string): Promise<IWorkflowBase>;
}
export interface IPollFunctions
extends FunctionsBaseWithRequiredKeys<'getMode' | 'getActivationMode'> {
__emit(
@@ -1293,14 +1307,18 @@ export interface INodePropertyTypeOptions {
resourceMapper?: ResourceMapperTypeOptions;
filter?: FilterTypeOptions;
assignment?: AssignmentTypeOptions;
minRequiredFields?: number; // Supported by: fixedCollection
maxAllowedFields?: number; // Supported by: fixedCollection
[key: string]: any;
}
export interface ResourceMapperTypeOptions {
resourceMapperMethod: string;
mode: 'add' | 'update' | 'upsert';
export interface ResourceMapperTypeOptionsBase {
mode: 'add' | 'update' | 'upsert' | 'map';
valuesLabel?: string;
fieldWords?: { singular: string; plural: string };
fieldWords?: {
singular: string;
plural: string;
};
addAllFields?: boolean;
noFieldsError?: string;
multiKeyMatch?: boolean;
@@ -1310,8 +1328,23 @@ export interface ResourceMapperTypeOptions {
description?: string;
hint?: string;
};
showTypeConversionOptions?: boolean;
}
// Enforce at least one of resourceMapperMethod or localResourceMapperMethod
export type ResourceMapperTypeOptionsLocal = {
resourceMapperMethod: string;
localResourceMapperMethod?: never; // Explicitly disallows this property
};
export type ResourceMapperTypeOptionsExternal = {
localResourceMapperMethod: string;
resourceMapperMethod?: never; // Explicitly disallows this property
};
export type ResourceMapperTypeOptions = ResourceMapperTypeOptionsBase &
(ResourceMapperTypeOptionsLocal | ResourceMapperTypeOptionsExternal);
type NonEmptyArray<T> = [T, ...T[]];
export type FilterTypeCombinator = 'and' | 'or';
@@ -1583,6 +1616,9 @@ export interface INodeType {
resourceMapping?: {
[functionName: string]: (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
};
localResourceMapping?: {
[functionName: string]: (this: ILocalLoadOptionsFunctions) => Promise<ResourceMapperFields>;
};
actionHandler?: {
[functionName: string]: (
this: ILoadOptionsFunctions,
@@ -2651,6 +2687,9 @@ export type ResourceMapperValue = {
value: { [key: string]: string | number | boolean | null } | null;
matchingColumns: string[];
schema: ResourceMapperField[];
ignoreTypeMismatchErrors: boolean;
attemptToConvertTypes: boolean;
convertFieldsToString: boolean;
};
export type FilterOperatorType =

View File

@@ -1568,7 +1568,7 @@ export function getParameterIssues(
data: option as INodeProperties,
});
}
} else if (nodeProperties.type === 'fixedCollection') {
} else if (nodeProperties.type === 'fixedCollection' && isDisplayed) {
basePath = basePath ? `${basePath}.` : `${nodeProperties.name}.`;
let propertyOptions: INodePropertyCollection;
@@ -1579,6 +1579,24 @@ export function getParameterIssues(
propertyOptions.name,
basePath.slice(0, -1),
);
// Validate allowed field counts
const valueArray = Array.isArray(value) ? value : [];
const { minRequiredFields, maxAllowedFields } = nodeProperties.typeOptions ?? {};
let error = '';
if (minRequiredFields && valueArray.length < minRequiredFields) {
error = `At least ${minRequiredFields} ${minRequiredFields === 1 ? 'field is' : 'fields are'} required.`;
}
if (maxAllowedFields && valueArray.length > maxAllowedFields) {
error = `At most ${maxAllowedFields} ${maxAllowedFields === 1 ? 'field is' : 'fields are'} allowed.`;
}
if (error) {
foundIssues.parameters ??= {};
foundIssues.parameters[nodeProperties.name] ??= [];
foundIssues.parameters[nodeProperties.name].push(error);
}
if (value === undefined) {
continue;
}

View File

@@ -974,7 +974,12 @@ export class WorkflowDataProxy {
type: 'no_execution_data',
});
}
return placeholdersDataInputData?.[name] ?? defaultValue;
return (
// TS does not know that the key exists, we need to address this in refactor
(placeholdersDataInputData?.query as Record<string, unknown>)?.[name] ??
placeholdersDataInputData?.[name] ??
defaultValue
);
};
const base = {

View File

@@ -1,5 +1,6 @@
import {
NodeConnectionType,
type INodeIssues,
type INode,
type INodeParameters,
type INodeProperties,
@@ -11,6 +12,7 @@ import {
getNodeHints,
isSubNodeType,
applyDeclarativeNodeOptionParameters,
getParameterIssues,
} from '@/NodeHelpers';
import type { Workflow } from '@/Workflow';
@@ -3607,4 +3609,590 @@ describe('NodeHelpers', () => {
expect(nodeType.description.properties).toEqual([]);
});
});
describe('getParameterIssues', () => {
const tests: Array<{
description: string;
input: {
nodeProperties: INodeProperties;
nodeValues: INodeParameters;
path: string;
node: INode;
};
output: INodeIssues;
}> = [
{
description:
'Fixed collection::Should not return issues if minimum or maximum field count is not set',
input: {
nodeProperties: {
displayName: 'Workflow Inputs',
name: 'workflowInputs',
placeholder: 'Add Field',
type: 'fixedCollection',
description:
'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.',
typeOptions: {
multipleValues: true,
sortable: true,
},
displayOptions: {
show: {
'@version': [
{
_cnd: {
gte: 1.1,
},
},
],
inputSource: ['workflowInputs'],
},
},
default: {},
options: [
{
name: 'values',
displayName: 'Values',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. fieldName',
description: 'Name of the field',
noDataExpression: true,
},
{
displayName: 'Type',
name: 'type',
type: 'options',
description: 'The field value type',
options: [
{
name: 'Allow Any Type',
value: 'any',
},
{
name: 'String',
value: 'string',
},
{
name: 'Number',
value: 'number',
},
{
name: 'Boolean',
value: 'boolean',
},
{
name: 'Array',
value: 'array',
},
{
name: 'Object',
value: 'object',
},
],
default: 'string',
noDataExpression: true,
},
],
},
],
},
nodeValues: {
events: 'worklfow_call',
inputSource: 'workflowInputs',
workflowInputs: {},
inputOptions: {},
},
path: '',
node: {
parameters: {
events: 'worklfow_call',
inputSource: 'workflowInputs',
workflowInputs: {},
inputOptions: {},
},
type: 'n8n-nodes-base.executeWorkflowTrigger',
typeVersion: 1.1,
position: [-140, -20],
id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00',
name: 'Test Node',
} as INode,
},
output: {},
},
{
description:
'Fixed collection::Should not return issues if field count is within the specified range',
input: {
nodeProperties: {
displayName: 'Workflow Inputs',
name: 'workflowInputs',
placeholder: 'Add Field',
type: 'fixedCollection',
description:
'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.',
typeOptions: {
multipleValues: true,
sortable: true,
minRequiredFields: 1,
maxAllowedFields: 3,
},
displayOptions: {
show: {
'@version': [
{
_cnd: {
gte: 1.1,
},
},
],
inputSource: ['workflowInputs'],
},
},
default: {},
options: [
{
name: 'values',
displayName: 'Values',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. fieldName',
description: 'Name of the field',
noDataExpression: true,
},
{
displayName: 'Type',
name: 'type',
type: 'options',
description: 'The field value type',
options: [
{
name: 'Allow Any Type',
value: 'any',
},
{
name: 'String',
value: 'string',
},
{
name: 'Number',
value: 'number',
},
{
name: 'Boolean',
value: 'boolean',
},
{
name: 'Array',
value: 'array',
},
{
name: 'Object',
value: 'object',
},
],
default: 'string',
noDataExpression: true,
},
],
},
],
},
nodeValues: {
events: 'worklfow_call',
inputSource: 'workflowInputs',
workflowInputs: {
values: [
{
name: 'field1',
type: 'string',
},
{
name: 'field2',
type: 'string',
},
],
},
inputOptions: {},
},
path: '',
node: {
parameters: {
events: 'worklfow_call',
inputSource: 'workflowInputs',
workflowInputs: {},
inputOptions: {},
},
type: 'n8n-nodes-base.executeWorkflowTrigger',
typeVersion: 1.1,
position: [-140, -20],
id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00',
name: 'Test Node',
} as INode,
},
output: {},
},
{
description:
'Fixed collection::Should return an issue if field count is lower than minimum specified',
input: {
nodeProperties: {
displayName: 'Workflow Inputs',
name: 'workflowInputs',
placeholder: 'Add Field',
type: 'fixedCollection',
description:
'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.',
typeOptions: {
multipleValues: true,
sortable: true,
minRequiredFields: 1,
},
displayOptions: {
show: {
'@version': [
{
_cnd: {
gte: 1.1,
},
},
],
inputSource: ['workflowInputs'],
},
},
default: {},
options: [
{
name: 'values',
displayName: 'Values',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. fieldName',
description: 'Name of the field',
noDataExpression: true,
},
{
displayName: 'Type',
name: 'type',
type: 'options',
description: 'The field value type',
options: [
{
name: 'Allow Any Type',
value: 'any',
},
{
name: 'String',
value: 'string',
},
{
name: 'Number',
value: 'number',
},
{
name: 'Boolean',
value: 'boolean',
},
{
name: 'Array',
value: 'array',
},
{
name: 'Object',
value: 'object',
},
],
default: 'string',
noDataExpression: true,
},
],
},
],
},
nodeValues: {
events: 'worklfow_call',
inputSource: 'workflowInputs',
workflowInputs: {},
inputOptions: {},
},
path: '',
node: {
parameters: {
events: 'worklfow_call',
inputSource: 'workflowInputs',
workflowInputs: {},
inputOptions: {},
},
type: 'n8n-nodes-base.executeWorkflowTrigger',
typeVersion: 1.1,
position: [-140, -20],
id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00',
name: 'Test Node',
} as INode,
},
output: {
parameters: {
workflowInputs: ['At least 1 field is required.'],
},
},
},
{
description:
'Fixed collection::Should return an issue if field count is higher than maximum specified',
input: {
nodeProperties: {
displayName: 'Workflow Inputs',
name: 'workflowInputs',
placeholder: 'Add Field',
type: 'fixedCollection',
description:
'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.',
typeOptions: {
multipleValues: true,
sortable: true,
maxAllowedFields: 1,
},
displayOptions: {
show: {
'@version': [
{
_cnd: {
gte: 1.1,
},
},
],
inputSource: ['workflowInputs'],
},
},
default: {},
options: [
{
name: 'values',
displayName: 'Values',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. fieldName',
description: 'Name of the field',
noDataExpression: true,
},
{
displayName: 'Type',
name: 'type',
type: 'options',
description: 'The field value type',
options: [
{
name: 'Allow Any Type',
value: 'any',
},
{
name: 'String',
value: 'string',
},
{
name: 'Number',
value: 'number',
},
{
name: 'Boolean',
value: 'boolean',
},
{
name: 'Array',
value: 'array',
},
{
name: 'Object',
value: 'object',
},
],
default: 'string',
noDataExpression: true,
},
],
},
],
},
nodeValues: {
events: 'worklfow_call',
inputSource: 'workflowInputs',
workflowInputs: {
values: [
{
name: 'field1',
type: 'string',
},
{
name: 'field2',
type: 'string',
},
],
},
inputOptions: {},
},
path: '',
node: {
parameters: {
events: 'worklfow_call',
inputSource: 'workflowInputs',
workflowInputs: {},
inputOptions: {},
},
type: 'n8n-nodes-base.executeWorkflowTrigger',
typeVersion: 1.1,
position: [-140, -20],
id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00',
name: 'Test Node',
} as INode,
},
output: {
parameters: {
workflowInputs: ['At most 1 field is allowed.'],
},
},
},
{
description: 'Fixed collection::Should not return issues if the collection is hidden',
input: {
nodeProperties: {
displayName: 'Workflow Inputs',
name: 'workflowInputs',
placeholder: 'Add Field',
type: 'fixedCollection',
description:
'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.',
typeOptions: {
multipleValues: true,
sortable: true,
maxAllowedFields: 1,
},
displayOptions: {
show: {
'@version': [
{
_cnd: {
gte: 1.1,
},
},
],
inputSource: ['workflowInputs'],
},
},
default: {},
options: [
{
name: 'values',
displayName: 'Values',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. fieldName',
description: 'Name of the field',
noDataExpression: true,
},
{
displayName: 'Type',
name: 'type',
type: 'options',
description: 'The field value type',
options: [
{
name: 'Allow Any Type',
value: 'any',
},
{
name: 'String',
value: 'string',
},
{
name: 'Number',
value: 'number',
},
{
name: 'Boolean',
value: 'boolean',
},
{
name: 'Array',
value: 'array',
},
{
name: 'Object',
value: 'object',
},
],
default: 'string',
noDataExpression: true,
},
],
},
],
},
nodeValues: {
events: 'worklfow_call',
inputSource: 'somethingElse',
workflowInputs: {
values: [
{
name: 'field1',
type: 'string',
},
{
name: 'field2',
type: 'string',
},
],
},
inputOptions: {},
},
path: '',
node: {
parameters: {
events: 'worklfow_call',
inputSource: 'workflowInputs',
workflowInputs: {},
inputOptions: {},
},
type: 'n8n-nodes-base.executeWorkflowTrigger',
typeVersion: 1.1,
position: [-140, -20],
id: '9abdbdac-5f32-4876-b4d5-895d8ca4cb00',
name: 'Test Node',
} as INode,
},
output: {},
},
];
for (const testData of tests) {
test(testData.description, () => {
const result = getParameterIssues(
testData.input.nodeProperties,
testData.input.nodeValues,
testData.input.path,
testData.input.node,
);
expect(result).toEqual(testData.output);
});
}
});
});