Merge remote-tracking branch 'origin/feature-sub-workflow-inputs' into ADO-2904

This commit is contained in:
Charlie Kolb 2024-11-27 14:51:15 +01:00
commit f716a607a5
No known key found for this signature in database

View file

@ -5,11 +5,14 @@ import {
type IExecuteFunctions, type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
validateFieldType,
type FieldType,
} from 'n8n-workflow'; } from 'n8n-workflow';
const INPUT_SOURCE = 'inputSource'; const INPUT_SOURCE = 'inputSource';
const FIELDS = 'fields'; const FIELDS = 'fields';
const WORKFLOW_INPUTS = 'workflowInputs'; const WORKFLOW_INPUTS = 'workflowInputs';
const INPUT_OPTIONS = 'inputOptions';
const VALUES = 'values'; const VALUES = 'values';
function hasFields(context: IExecuteFunctions, index: number): boolean { function hasFields(context: IExecuteFunctions, index: number): boolean {
@ -29,8 +32,9 @@ function parseJson(
index: number, index: number,
): Array<{ ): Array<{
name: string; name: string;
type: FieldType;
}> { }> {
return [{ name: 'dummy' }]; return [{ name: 'dummy', type: 'number' }];
} }
function getSchema( function getSchema(
@ -38,17 +42,22 @@ function getSchema(
index: number, index: number,
): Array<{ ): Array<{
name: string; name: string;
type: FieldType;
}> { }> {
const inputSource = context.getNodeParameter(INPUT_SOURCE, index) as string; const inputSource = context.getNodeParameter(INPUT_SOURCE, index) as string;
if (inputSource === FIELDS) { if (inputSource === FIELDS) {
const fields = context.getNodeParameter(`${WORKFLOW_INPUTS}.${VALUES}`, index, []) as Array<{ const fields = context.getNodeParameter(`${WORKFLOW_INPUTS}.${VALUES}`, index, []) as Array<{
name: string; name: string;
type: FieldType;
}>; }>;
return fields; return fields;
} else { } else {
return parseJson(context, index); return parseJson(context, index);
} }
} }
type ValueOptions = { name: string; value: FieldType };
const DEFAULT_PLACEHOLDER = null;
export class ExecuteWorkflowTrigger implements INodeType { export class ExecuteWorkflowTrigger implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -69,8 +78,9 @@ export class ExecuteWorkflowTrigger implements INodeType {
outputs: [NodeConnectionType.Main], outputs: [NodeConnectionType.Main],
properties: [ properties: [
{ {
displayName: displayName: `When an Execute Workflow node calls this workflow, the execution starts here.<br><br>
"When an execute workflow node calls this workflow, the execution starts here. Any data passed into the 'execute workflow' node will be output by this node.", Specified fields below will be output by this node with values provided by the calling workflow.<br><br>
If you don't provide fields, all data passed into the 'Execute Workflow' node will be passed through instead.`,
name: 'notice', name: 'notice',
type: 'notice', type: 'notice',
default: '', default: '',
@ -139,40 +149,93 @@ export class ExecuteWorkflowTrigger implements INodeType {
description: 'Name of the field', description: 'Name of the field',
noDataExpression: true, noDataExpression: true,
}, },
// { {
// displayName: 'Type', displayName: 'Type',
// name: 'type', name: 'type',
// type: 'options', type: 'options',
// description: 'The field value type', description: 'The field value type',
// // eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items // eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
// options: [ options: [
// { // This is not a FieldType type, but will
// name: 'String', // hit the default case in the type check function
// value: 'stringValue', {
// }, name: 'Allow Any Type',
// { value: 'any',
// name: 'Number', },
// value: 'numberValue', {
// }, name: 'String',
// { value: 'string',
// name: 'Boolean', },
// value: 'booleanValue', {
// }, name: 'Number',
// { value: 'number',
// name: 'Array', },
// value: 'arrayValue', {
// }, name: 'Boolean',
// { value: 'boolean',
// name: 'Object', },
// value: 'objectValue', {
// }, name: 'Array',
// ], value: 'array',
// default: 'stringValue', },
// }, {
name: 'Object',
value: 'object',
},
// Intentional omission of `dateTime`, `time`, `string-alphanumeric`, `form-fields`, `jwt` and `url`
] as ValueOptions[],
default: 'string',
noDataExpression: true,
},
], ],
}, },
], ],
}, },
{
displayName: 'Input Options',
name: INPUT_OPTIONS,
placeholder: 'Options',
type: 'collection',
description: 'Options controlling how input data is handled, converted and rejected',
displayOptions: {
show: { '@version': [{ _cnd: { gte: 1.1 } }] },
},
default: {},
// Note that, while the defaults are true, the user has to add these in the first place
// We default to false if absent in the execute function below
options: [
{
displayName: 'Attempt to Convert Types',
name: 'attemptToConvertTypes',
type: 'boolean',
default: true,
description:
'Whether to attempt conversion on type mismatch, rather than directly returning an Error',
noDataExpression: true,
},
{
displayName: 'Ignore Type Mismatch Errors',
name: 'ignoreTypeErrors',
type: 'boolean',
default: true,
description: 'Whether type mismatches should be ignored rather than returning an Error',
noDataExpression: true,
},
// REVIEW: Note that by having this here we commit to passing the binary data
// to the sub-workflow in the first place, otherwise we'd need this on the parent
// or at least for the parent to read this from this node.
// Is there significant cost to switching to the sub-workflow or is it all one big workflow under the hood?
{
displayName: 'Include Binary Data',
name: 'includeBinaryData',
type: 'boolean',
default: true,
description:
'Whether binary data should be included from the parent. If set to false, binary data will be removed.',
noDataExpression: true,
},
],
},
], ],
}; };
@ -189,9 +252,26 @@ export class ExecuteWorkflowTrigger implements INodeType {
const items: INodeExecutionData[] = []; const items: INodeExecutionData[] = [];
for (const [itemIndex, item] of inputData.entries()) { for (const [itemIndex, item] of inputData.entries()) {
const attemptToConvertTypes = this.getNodeParameter(
`${INPUT_OPTIONS}.attemptToConvertTypes`,
itemIndex,
false,
);
const ignoreTypeErrors = this.getNodeParameter(
`${INPUT_OPTIONS}.ignoreTypeErrors`,
itemIndex,
false,
);
const includeBinaryData = this.getNodeParameter(
`${INPUT_OPTIONS}.includeBinaryData`,
itemIndex,
false,
);
// Fields listed here will explicitly overwrite original fields // Fields listed here will explicitly overwrite original fields
const newItem: INodeExecutionData = { const newItem: INodeExecutionData = {
json: {}, json: {},
index: itemIndex,
// TODO: Ensure we handle sub-execution jumps correctly. // TODO: Ensure we handle sub-execution jumps correctly.
// metadata: { // metadata: {
// subExecution: { // subExecution: {
@ -204,13 +284,47 @@ export class ExecuteWorkflowTrigger implements INodeType {
try { try {
const newParams = getSchema(this, itemIndex); const newParams = getSchema(this, itemIndex);
for (const { name } of newParams) { for (const { name, type } of newParams) {
/** TODO type check goes here */ if (!item.json.hasOwnProperty(name)) {
newItem.json[name] = name in item.json ? item.json[name] : /* TODO default */ null; newItem.json[name] = DEFAULT_PLACEHOLDER;
continue;
}
// We always parse strings rather than blindly accepting anything as a string
// Which is the behavior of this function
// Also note we intentionally pass `any` in here for `type`, which hits a
// permissive default case in the function
const result = validateFieldType(name, item.json[name], type, {
strict: !attemptToConvertTypes,
parseStrings: true,
});
if (!result.valid) {
if (ignoreTypeErrors) {
newItem.json[name] = item.json[name];
continue;
}
throw new NodeOperationError(this.getNode(), result.errorMessage, {
itemIndex,
});
} else {
// If the value is `null` or `undefined`, then `newValue` is not in the returned object
if (result.hasOwnProperty('newValue')) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
newItem.json[name] = result.newValue;
} else {
newItem.json[name] = item.json[name];
}
}
} }
// TODO Do we want to copy non-json data (e.g. binary) as well? if (includeBinaryData) {
items.push(Object.assign({}, item, newItem)); // Important not to assign directly to avoid modifying upstream data
items.push(Object.assign({}, item, newItem));
} else {
items.push(newItem);
}
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
/** todo error case? */ /** todo error case? */