diff --git a/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts b/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts index 1f771928ba..fd64cc6121 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts @@ -5,11 +5,14 @@ import { type IExecuteFunctions, type INodeType, type INodeTypeDescription, + validateFieldType, + type FieldType, } from 'n8n-workflow'; const INPUT_SOURCE = 'inputSource'; const FIELDS = 'fields'; const WORKFLOW_INPUTS = 'workflowInputs'; +const INPUT_OPTIONS = 'inputOptions'; const VALUES = 'values'; function hasFields(context: IExecuteFunctions, index: number): boolean { @@ -29,8 +32,9 @@ function parseJson( index: number, ): Array<{ name: string; + type: FieldType; }> { - return [{ name: 'dummy' }]; + return [{ name: 'dummy', type: 'number' }]; } function getSchema( @@ -38,17 +42,22 @@ function getSchema( index: number, ): Array<{ name: string; + type: FieldType; }> { const inputSource = context.getNodeParameter(INPUT_SOURCE, index) as string; if (inputSource === FIELDS) { const fields = context.getNodeParameter(`${WORKFLOW_INPUTS}.${VALUES}`, index, []) as Array<{ name: string; + type: FieldType; }>; return fields; } else { return parseJson(context, index); } } +type ValueOptions = { name: string; value: FieldType }; + +const DEFAULT_PLACEHOLDER = null; export class ExecuteWorkflowTrigger implements INodeType { description: INodeTypeDescription = { @@ -69,8 +78,9 @@ export class ExecuteWorkflowTrigger implements INodeType { outputs: [NodeConnectionType.Main], properties: [ { - displayName: - "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.", + displayName: `When an ‘Execute Workflow’ node calls this workflow, the execution starts here.

+Specified fields below will be output by this node with values provided by the calling workflow.

+If you don't provide fields, all data passed into the 'Execute Workflow' node will be passed through instead.`, name: 'notice', type: 'notice', default: '', @@ -139,40 +149,93 @@ export class ExecuteWorkflowTrigger implements INodeType { description: 'Name of the field', noDataExpression: true, }, - // { - // displayName: 'Type', - // name: 'type', - // type: 'options', - // description: 'The field value type', - // // eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items - // options: [ - // { - // name: 'String', - // value: 'stringValue', - // }, - // { - // name: 'Number', - // value: 'numberValue', - // }, - // { - // name: 'Boolean', - // value: 'booleanValue', - // }, - // { - // name: 'Array', - // value: 'arrayValue', - // }, - // { - // name: 'Object', - // value: 'objectValue', - // }, - // ], - // default: 'stringValue', - // }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The field value type', + // eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items + options: [ + // This is not a FieldType type, but will + // hit the default case in the type check function + { + 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', + }, + // 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[] = []; 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 const newItem: INodeExecutionData = { json: {}, + index: itemIndex, // TODO: Ensure we handle sub-execution jumps correctly. // metadata: { // subExecution: { @@ -204,13 +284,47 @@ export class ExecuteWorkflowTrigger implements INodeType { try { const newParams = getSchema(this, itemIndex); - for (const { name } of newParams) { - /** TODO type check goes here */ - newItem.json[name] = name in item.json ? item.json[name] : /* TODO default */ null; + for (const { name, type } of newParams) { + if (!item.json.hasOwnProperty(name)) { + 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? - items.push(Object.assign({}, item, newItem)); + if (includeBinaryData) { + // Important not to assign directly to avoid modifying upstream data + items.push(Object.assign({}, item, newItem)); + } else { + items.push(newItem); + } } catch (error) { if (this.continueOnFail()) { /** todo error case? */