diff --git a/cypress/e2e/48-subworkflow-inputs.cy.ts b/cypress/e2e/48-subworkflow-inputs.cy.ts index d3315f6cca724..02eff605f8494 100644 --- a/cypress/e2e/48-subworkflow-inputs.cy.ts +++ b/cypress/e2e/48-subworkflow-inputs.cy.ts @@ -107,6 +107,7 @@ function validateAndReturnToParent(targetChild: string, offset: number, fields: // Due to our workaround to remain in the same tab we need to select the correct tab manually navigateWorkflowSelectionDropdown(offset, targetChild); + // This fails, pointing to `usePushConnection` `const triggerNode = subWorkflow?.nodes.find` being `undefined.find()`I ndv.actions.execute(); getOutputTableHeaders().should('have.length', fields.length + 1); diff --git a/cypress/fixtures/Test_Subworkflow-Inputs.json b/cypress/fixtures/Test_Subworkflow-Inputs.json index 1b2320510fdea..5b96c0e3f2c8e 100644 --- a/cypress/fixtures/Test_Subworkflow-Inputs.json +++ b/cypress/fixtures/Test_Subworkflow-Inputs.json @@ -23,13 +23,23 @@ }, { "parameters": { + "workflowId": {}, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": {}, + "matchingColumns": [], + "schema": [], + "ignoreTypeMismatchErrors": false, + "attemptToConvertTypes": false, + "convertFieldsToString": true + }, "options": {} }, - "id": "ddc82976-edd9-4488-a5a5-7f558a7d905b", - "name": "Execute Workflow", "type": "n8n-nodes-base.executeWorkflow", - "typeVersion": 1.1, - "position": [500, 240] + "typeVersion": 1.2, + "position": [500, 240], + "id": "6b6e2e34-c6ab-4083-b8e3-6b0d56be5453", + "name": "Execute Workflow" } ], "connections": { diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts index 5ab1604262bc4..22ca31e4da2b1 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts @@ -1,3 +1,4 @@ +import { loadWorkflowInputMappings } from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions'; import type { INodeTypeBaseDescription, ISupplyDataFunctions, @@ -6,7 +7,6 @@ import type { INodeTypeDescription, } from 'n8n-workflow'; -import { loadWorkflowInputMappings } from './methods/resourceMapping'; import { WorkflowToolService } from './utils/WorkflowToolService'; import { versionDescription } from './versionDescription'; diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/resourceMapping.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/resourceMapping.ts deleted file mode 100644 index e0c0b0d6650d2..0000000000000 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/resourceMapping.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - getFieldEntries, - getWorkflowInputData, -} from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions'; -import type { - ISupplyDataFunctions, - IDataObject, - FieldValueOption, - ResourceMapperField, - ILocalLoadOptionsFunctions, - ResourceMapperFields, -} from 'n8n-workflow'; -import { EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE } from 'n8n-workflow'; - -export async function loadWorkflowInputMappings( - this: ILocalLoadOptionsFunctions, -): Promise { - const nodeLoadContext = await this.getWorkflowNodeContext(EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE); - let fields: ResourceMapperField[] = []; - if (nodeLoadContext) { - const fieldValues = getFieldEntries(nodeLoadContext); - fields = fieldValues.map((currentWorkflowInput) => { - const field: ResourceMapperField = { - id: currentWorkflowInput.name, - displayName: currentWorkflowInput.name, - required: false, - defaultMatch: false, - display: true, - canBeUsedToMatch: true, - }; - - if (currentWorkflowInput.type !== 'any') { - field.type = currentWorkflowInput.type; - } - - return field; - }); - } - return { fields }; -} - -export function getWorkflowInputValues(this: ISupplyDataFunctions) { - const inputData = this.getInputData(); - - return inputData.map((item, itemIndex) => { - const itemFieldValues = this.getNodeParameter( - 'workflowInputs.value', - itemIndex, - {}, - ) as IDataObject; - - return { - json: { - ...item.json, - ...itemFieldValues, - }, - index: itemIndex, - pairedItem: { - item: itemIndex, - }, - }; - }); -} - -export function getCurrentWorkflowInputData(this: ISupplyDataFunctions) { - const inputData = getWorkflowInputValues.call(this); - - const schema = this.getNodeParameter('workflowInputs.schema', 0, []) as ResourceMapperField[]; - - if (schema.length === 0) { - return inputData; - } else { - const newParams = schema - .filter((x) => !x.removed) - .map((x) => ({ name: x.displayName, type: x.type ?? 'any' })) as FieldValueOption[]; - - return getWorkflowInputData.call(this, inputData, newParams); - } -} diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts index 7ea2bf0284174..5412f17ccd46c 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts @@ -4,6 +4,7 @@ import get from 'lodash/get'; import isObject from 'lodash/isObject'; import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces'; import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode'; +import { getCurrentWorkflowInputData } from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions'; import type { ExecuteWorkflowData, ExecutionError, @@ -22,7 +23,6 @@ import { z } from 'zod'; import type { FromAIArgument } from './FromAIParser'; import { AIParametersParser } from './FromAIParser'; -import { getCurrentWorkflowInputData } from '../methods/resourceMapping'; /** Main class for creating the Workflow tool diff --git a/packages/core/src/node-execution-context/utils.ts b/packages/core/src/node-execution-context/utils.ts index a09147d543b23..bcde2fc6d7365 100644 --- a/packages/core/src/node-execution-context/utils.ts +++ b/packages/core/src/node-execution-context/utils.ts @@ -83,7 +83,6 @@ const validateResourceMapperValue = ( for (let i = 0; i < paramValueNames.length; i++) { const key = paramValueNames[i]; const resolvedValue = paramValues[key]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call const schemaEntry = schema.find((s) => s.id === key); if ( @@ -99,15 +98,19 @@ const validateResourceMapperValue = ( }; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (schemaEntry?.type) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const validationResult = validateFieldType(key, resolvedValue, schemaEntry.type, { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access valueOptions: schemaEntry.options, + strict: !resourceMapperField.attemptToConvertTypes, + parseStrings: !!resourceMapperField.convertFieldsToString, }); + if (!validationResult.valid) { - return { ...validationResult, fieldName: key }; + if (!resourceMapperField.ignoreTypeMismatchErrors) { + return { ...validationResult, fieldName: key }; + } else { + paramValues[key] = resolvedValue; + } } else { // If it's valid, set the casted value paramValues[key] = validationResult.newValue; diff --git a/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue b/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue index 2bc34a447582d..8ae72a418df3b 100644 --- a/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue +++ b/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue @@ -7,6 +7,7 @@ import type { INodeParameters, INodeProperties, INodeTypeDescription, + NodeParameterValueType, ResourceMapperField, ResourceMapperValue, } from 'n8n-workflow'; @@ -52,6 +53,12 @@ const state = reactive({ value: {}, matchingColumns: [] as string[], schema: [] as ResourceMapperField[], + ignoreTypeMismatchErrors: false, + attemptToConvertTypes: false, + // This should always be true if `showTypeConversionOptions` is provided + // It's used to avoid accepting any value as string without casting it + // Which is the legacy behavior without these type options. + convertFieldsToString: false, } as ResourceMapperValue, parameterValues: {} as INodeParameters, loading: false, @@ -97,6 +104,10 @@ onMounted(async () => { ...state.parameterValues, parameters: props.node.parameters, }; + + if (showTypeConversionOptions.value) { + state.paramValue.convertFieldsToString = true; + } } const params = state.parameterValues.parameters as INodeParameters; const parameterName = props.parameter.name; @@ -161,6 +172,10 @@ const showMappingModeSelect = computed(() => { return props.parameter.typeOptions?.resourceMapper?.supportAutoMap !== false; }); +const showTypeConversionOptions = computed(() => { + return props.parameter.typeOptions?.resourceMapper?.showTypeConversionOptions === true; +}); + const showMatchingColumnsSelector = computed(() => { return ( !state.loading && @@ -572,5 +587,52 @@ defineExpose({ }) }} +
+ + +
+ + diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 638000b2198ad..d8084b9eed30c 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1588,6 +1588,10 @@ "resourceMapper.addAllFields": "Add All {fieldWord}", "resourceMapper.removeAllFields": "Remove All {fieldWord}", "resourceMapper.refreshFieldList": "Refresh {fieldWord} List", + "resourceMapper.attemptToConvertTypes.displayName": "Attempt to convert types", + "resourceMapper.attemptToConvertTypes.description": "Attempt to convert types when mapping fields", + "resourceMapper.ignoreTypeMismatchErrors.displayName": "Ignore type mismatch errors", + "resourceMapper.ignoreTypeMismatchErrors.description": "Whether type mismatches should be ignored, rather than returning an Error", "runData.openSubExecution": "Inspect Sub-Execution {id}", "runData.openParentExecution": "Inspect Parent Execution {id}", "runData.emptyItemHint": "This is an item, but it's empty.", diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts index 121a5ad38bc26..2f67d7bfe9b6f 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts @@ -1,59 +1,18 @@ import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import type { ExecuteWorkflowData, - FieldValueOption, - IDataObject, IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription, - ResourceMapperField, } from 'n8n-workflow'; import { getWorkflowInfo } from './GenericFunctions'; -import { loadWorkflowInputMappings } from './methods/resourceMapping'; import { generatePairedItemData } from '../../../utils/utilities'; -import { getWorkflowInputData } from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; - -function getWorkflowInputValues(this: IExecuteFunctions) { - const inputData = this.getInputData(); - - return inputData.map((item, itemIndex) => { - const itemFieldValues = this.getNodeParameter( - 'workflowInputs.value', - itemIndex, - {}, - ) as IDataObject; - - return { - json: { - ...item.json, - ...itemFieldValues, - }, - index: itemIndex, - pairedItem: { - item: itemIndex, - }, - }; - }); -} - -function getCurrentWorkflowInputData(this: IExecuteFunctions) { - const inputData = getWorkflowInputValues.call(this); - - const schema = this.getNodeParameter('workflowInputs.schema', 0, []) as ResourceMapperField[]; - - if (schema.length === 0) { - return inputData; - } else { - const newParams = schema - .filter((x) => !x.removed) - .map((x) => ({ name: x.displayName, type: x.type ?? 'any' })) as FieldValueOption[]; - - return getWorkflowInputData.call(this, inputData, newParams); - } -} - +import { + getCurrentWorkflowInputData, + loadWorkflowInputMappings, +} from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; export class ExecuteWorkflow implements INodeType { description: INodeTypeDescription = { displayName: 'Execute Workflow', @@ -84,6 +43,13 @@ export class ExecuteWorkflow implements INodeType { }, ], }, + { + displayName: 'This node is out of date. Please upgrade by removing it and adding a new one', + name: 'outdatedVersionWarning', + type: 'notice', + displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } }, + default: '', + }, { displayName: 'Source', name: 'source', @@ -254,6 +220,7 @@ export class ExecuteWorkflow implements INodeType { addAllFields: true, multiKeyMatch: false, supportAutoMap: false, + showTypeConversionOptions: true, }, }, displayOptions: { diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/methods/resourceMapping.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/methods/resourceMapping.ts deleted file mode 100644 index c747f6f505b9d..0000000000000 --- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/methods/resourceMapping.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, - type ILocalLoadOptionsFunctions, - type ResourceMapperField, - type ResourceMapperFields, -} from 'n8n-workflow'; - -import { getFieldEntries } from '../../../../utils/workflowInputsResourceMapping/GenericFunctions'; - -export async function loadWorkflowInputMappings( - this: ILocalLoadOptionsFunctions, -): Promise { - const nodeLoadContext = await this.getWorkflowNodeContext(EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE); - let fields: ResourceMapperField[] = []; - - if (nodeLoadContext) { - const fieldValues = getFieldEntries(nodeLoadContext); - - fields = fieldValues.map((currentWorkflowInput) => { - const field: ResourceMapperField = { - id: currentWorkflowInput.name, - displayName: currentWorkflowInput.name, - required: false, - defaultMatch: false, - display: true, - canBeUsedToMatch: true, - }; - - if (currentWorkflowInput.type !== 'any') { - field.type = currentWorkflowInput.type; - } - - return field; - }); - } - return { fields }; -} diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.test.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.test.ts index 953f78fd21d3b..b479538c3a727 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.test.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.test.ts @@ -2,10 +2,8 @@ import { mock } from 'jest-mock-extended'; import type { FieldValueOption, IExecuteFunctions, INode, INodeExecutionData } from 'n8n-workflow'; import { ExecuteWorkflowTrigger } from './ExecuteWorkflowTrigger.node'; -import { - getFieldEntries, - getWorkflowInputData, -} from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; +import { WORKFLOW_INPUTS } from '../../../utils/workflowInputsResourceMapping/constants'; +import { getFieldEntries } from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; jest.mock('../../../utils/workflowInputsResourceMapping/GenericFunctions', () => ({ getFieldEntries: jest.fn(), @@ -14,8 +12,8 @@ jest.mock('../../../utils/workflowInputsResourceMapping/GenericFunctions', () => describe('ExecuteWorkflowTrigger', () => { const mockInputData: INodeExecutionData[] = [ - { json: { item: 0, foo: 'bar' } }, - { json: { item: 1, foo: 'quz' } }, + { json: { item: 0, foo: 'bar' }, index: 0 }, + { json: { item: 1, foo: 'quz' }, index: 1 }, ]; const mockNode = mock({ typeVersion: 1 }); const executeFns = mock({ @@ -24,28 +22,32 @@ describe('ExecuteWorkflowTrigger', () => { getNodeParameter: jest.fn(), }); - it('should return its input data on V1', async () => { + it('should return its input data on V1 or V1.1 passthrough', async () => { + // User selection in V1.1, or fallback return value in V1 with dropdown not displayed executeFns.getNodeParameter.mockReturnValueOnce('passthrough'); const result = await new ExecuteWorkflowTrigger().execute.call(executeFns); expect(result).toEqual([mockInputData]); }); - it('should return transformed input data based on newParams when input source is not passthrough', async () => { - executeFns.getNodeParameter.mockReturnValueOnce('usingFieldsBelow'); + it('should filter out parent input in `Using Fields below` mode', async () => { + executeFns.getNodeParameter.mockReturnValueOnce(WORKFLOW_INPUTS); const mockNewParams = [ { name: 'value1', type: 'string' }, { name: 'value2', type: 'number' }, + { name: 'foo', type: 'string' }, ] as FieldValueOption[]; const getFieldEntriesMock = (getFieldEntries as jest.Mock).mockReturnValue(mockNewParams); - const getWorkflowInputDataMock = (getWorkflowInputData as jest.Mock).mockReturnValue( - mockInputData, - ); const result = await new ExecuteWorkflowTrigger().execute.call(executeFns); - - expect(result).toEqual([mockInputData]); + const expected = [ + [ + { index: 0, json: { value1: null, value2: null, foo: mockInputData[0].json.foo } }, + { index: 1, json: { value1: null, value2: null, foo: mockInputData[1].json.foo } }, + ], + ]; + + expect(result).toEqual(expected); expect(getFieldEntriesMock).toHaveBeenCalledWith(executeFns); - expect(getWorkflowInputDataMock).toHaveBeenCalledWith(mockInputData, mockNewParams); }); }); diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts index 0c8e5f64ce7cf..b2c4321cdb666 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts @@ -1,4 +1,6 @@ +import _ from 'lodash'; import { + type INodeExecutionData, NodeConnectionType, type IExecuteFunctions, type INodeType, @@ -10,14 +12,11 @@ import { WORKFLOW_INPUTS, JSON_EXAMPLE, VALUES, - INPUT_OPTIONS, TYPE_OPTIONS, PASSTHROUGH, + FALLBACK_DEFAULT_VALUE, } from '../../../utils/workflowInputsResourceMapping/constants'; -import { - getFieldEntries, - getWorkflowInputData, -} from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; +import { getFieldEntries } from '../../../utils/workflowInputsResourceMapping/GenericFunctions'; export class ExecuteWorkflowTrigger implements INodeType { description: INodeTypeDescription = { @@ -66,6 +65,23 @@ export class ExecuteWorkflowTrigger implements INodeType { ], default: 'worklfow_call', }, + { + 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.", + name: 'notice', + type: 'notice', + default: '', + displayOptions: { + show: { '@version': [{ _cnd: { eq: 1 } }] }, + }, + }, + { + displayName: 'This node is out of date. Please upgrade by removing it and adding a new one', + name: 'outdatedVersionWarning', + type: 'notice', + displayOptions: { show: { '@version': [{ _cnd: { eq: 1 } }] } }, + default: '', + }, { displayName: 'Input Source', name: INPUT_SOURCE, @@ -166,39 +182,6 @@ export class ExecuteWorkflowTrigger implements INodeType { }, ], }, - { - 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, - }, - ], - }, ], }; @@ -206,12 +189,33 @@ export class ExecuteWorkflowTrigger implements INodeType { const inputData = this.getInputData(); const inputSource = this.getNodeParameter(INPUT_SOURCE, 0, PASSTHROUGH) as string; + // Note on the data we receive from ExecuteWorkflow caller: + // + // The ExecuteWorkflow node typechecks all fields explicitly provided by the user here via the resourceMapper + // and removes all fields that are in the schema, but `removed` in the resourceMapper. + // + // In passthrough and legacy node versions, inputData will line up since the resourceMapper is empty, + // in which case all input is passed through. + // In other cases we will already have matching types and fields provided by the resource mapper, + // so we just need to be permissive on this end, + // while ensuring we provide default values for fields in our schema, which are removed in the resourceMapper. + if (inputSource === PASSTHROUGH) { return [inputData]; } else { const newParams = getFieldEntries(this); + const newKeys = new Set(newParams.map((x) => x.name)); + const itemsInSchema: INodeExecutionData[] = inputData.map((row, index) => ({ + json: { + ...Object.fromEntries(newParams.map((x) => [x.name, FALLBACK_DEFAULT_VALUE])), + // Need to trim to the expected schema to support legacy Execute Workflow callers passing through all their data + // which we do not want to expose past this node. + ..._.pickBy(row.json, (_value, key) => newKeys.has(key)), + }, + index, + })); - return [getWorkflowInputData.call(this, inputData, newParams)]; + return [itemsInSchema]; } } } diff --git a/packages/nodes-base/utils/workflowInputsResourceMapping/.readme b/packages/nodes-base/utils/workflowInputsResourceMapping/.readme index d92560581d2fe..e5556cc0cccaf 100644 --- a/packages/nodes-base/utils/workflowInputsResourceMapping/.readme +++ b/packages/nodes-base/utils/workflowInputsResourceMapping/.readme @@ -2,4 +2,4 @@ These files contain reusable logic for workflow inputs mapping used in these nod - n8n-nodes-base.executeWorkflow - n8n-nodes-base.executeWorkflowTrigger - - @n8n/n8n-nodes-langchain.toolWorkflow" + - @n8n/n8n-nodes-langchain.toolWorkflow diff --git a/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts b/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts index 88aa5e971cf87..8708b8e18aa8a 100644 --- a/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts +++ b/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts @@ -1,14 +1,20 @@ import { json as generateSchemaFromExample, type SchemaObject } from 'generate-schema'; import type { JSONSchema7 } from 'json-schema'; -import type { - FieldValueOption, - FieldType, - IWorkflowNodeContext, - INodeExecutionData, - IExecuteFunctions, - ISupplyDataFunctions, +import _ from 'lodash'; +import { + type FieldValueOption, + type FieldType, + type IWorkflowNodeContext, + jsonParse, + NodeOperationError, + type INodeExecutionData, + type IDataObject, + type ResourceMapperField, + type ILocalLoadOptionsFunctions, + type ResourceMapperFields, + EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, + type ISupplyDataFunctions, } from 'n8n-workflow'; -import { jsonParse, NodeOperationError, validateFieldType } from 'n8n-workflow'; import { JSON_EXAMPLE, @@ -16,8 +22,6 @@ import { WORKFLOW_INPUTS, VALUES, TYPE_OPTIONS, - INPUT_OPTIONS, - FALLBACK_DEFAULT_VALUE, PASSTHROUGH, } from './constants'; @@ -92,84 +96,74 @@ export function getFieldEntries(context: IWorkflowNodeContext): FieldValueOption throw new NodeOperationError(context.getNode(), result); } -export function getWorkflowInputData( - this: IExecuteFunctions | ISupplyDataFunctions, - inputData: INodeExecutionData[], - newParams: FieldValueOption[], -): INodeExecutionData[] { - const items: INodeExecutionData[] = []; +export function getWorkflowInputValues(this: ISupplyDataFunctions): INodeExecutionData[] { + const inputData = this.getInputData(); - for (const [itemIndex, item] of inputData.entries()) { - const attemptToConvertTypes = this.getNodeParameter( - `${INPUT_OPTIONS}.attemptToConvertTypes`, - itemIndex, - false, - ); - const ignoreTypeErrors = this.getNodeParameter( - `${INPUT_OPTIONS}.ignoreTypeErrors`, + return inputData.map((item, itemIndex) => { + const itemFieldValues = this.getNodeParameter( + 'workflowInputs.value', itemIndex, - false, - ); - - // Fields listed here will explicitly overwrite original fields - const newItem: INodeExecutionData = { - json: {}, + {}, + ) as IDataObject; + + return { + json: { + ...item.json, + ...itemFieldValues, + }, index: itemIndex, - // TODO: Ensure we handle sub-execution jumps correctly. - // metadata: { - // subExecution: { - // executionId: 'uhh', - // workflowId: 'maybe?', - // }, - // }, - pairedItem: { item: itemIndex }, + pairedItem: { + item: itemIndex, + }, }; - try { - for (const { name, type } of newParams) { - if (!item.json.hasOwnProperty(name)) { - newItem.json[name] = FALLBACK_DEFAULT_VALUE; - continue; - } - - const result = - type === 'any' - ? ({ valid: true, newValue: item.json[name] } as const) - : validateFieldType(name, item.json[name], type, { - strict: !attemptToConvertTypes, - parseStrings: true, // Default behavior is to accept anything as a string, this is a good opportunity for a stricter boundary - }); - - 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]; - } - } - } + }); +} - items.push(newItem); - } catch (error) { - if (this.continueOnFail()) { - /** todo error case? */ - } else { - throw new NodeOperationError(this.getNode(), error, { - itemIndex, - }); - } - } +export function getCurrentWorkflowInputData(this: ISupplyDataFunctions) { + const inputData: INodeExecutionData[] = getWorkflowInputValues.call(this); + + const schema = this.getNodeParameter('workflowInputs.schema', 0, []) as ResourceMapperField[]; + + if (schema.length === 0) { + return inputData; + } else { + const removedKeys = new Set(schema.filter((x) => x.removed).map((x) => x.displayName)); + + const filteredInputData: INodeExecutionData[] = inputData.map((item, index) => ({ + index, + pairedItem: { item: index }, + json: _.pickBy(item.json, (_v, key) => !removedKeys.has(key)), + })); + + return filteredInputData; } +} - return items; +export async function loadWorkflowInputMappings( + this: ILocalLoadOptionsFunctions, +): Promise { + const nodeLoadContext = await this.getWorkflowNodeContext(EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE); + let fields: ResourceMapperField[] = []; + + if (nodeLoadContext) { + const fieldValues = getFieldEntries(nodeLoadContext); + + fields = fieldValues.map((currentWorkflowInput) => { + const field: ResourceMapperField = { + id: currentWorkflowInput.name, + displayName: currentWorkflowInput.name, + required: false, + defaultMatch: false, + display: true, + canBeUsedToMatch: true, + }; + + if (currentWorkflowInput.type !== 'any') { + field.type = currentWorkflowInput.type; + } + + return field; + }); + } + return { fields }; } diff --git a/packages/nodes-base/utils/workflowInputsResourceMapping/constants.ts b/packages/nodes-base/utils/workflowInputsResourceMapping/constants.ts index 69bb41af7397f..409d8d703ecab 100644 --- a/packages/nodes-base/utils/workflowInputsResourceMapping/constants.ts +++ b/packages/nodes-base/utils/workflowInputsResourceMapping/constants.ts @@ -2,7 +2,6 @@ import type { FieldType } from 'n8n-workflow'; export const INPUT_SOURCE = 'inputSource'; export const WORKFLOW_INPUTS = 'workflowInputs'; -export const INPUT_OPTIONS = 'inputOptions'; export const VALUES = 'values'; export const JSON_EXAMPLE = 'jsonExample'; export const PASSTHROUGH = 'passthrough'; diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 8689999a6b2a7..4fe84136c303a 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1328,6 +1328,7 @@ export interface ResourceMapperTypeOptionsBase { description?: string; hint?: string; }; + showTypeConversionOptions?: boolean; } // Enforce at least one of resourceMapperMethod or localResourceMapperMethod @@ -2665,6 +2666,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 = diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba2200020b469..955d19b18e3ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -283,7 +283,7 @@ importers: version: 4.0.7 axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 dotenv: specifier: 8.6.0 version: 8.6.0 @@ -354,7 +354,7 @@ importers: dependencies: axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 packages/@n8n/codemirror-lang: dependencies: @@ -428,7 +428,7 @@ importers: version: 3.666.0(@aws-sdk/client-sts@3.666.0) '@getzep/zep-cloud': specifier: 1.0.12 - version: 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i)) + version: 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu)) '@getzep/zep-js': specifier: 0.9.0 version: 0.9.0 @@ -455,7 +455,7 @@ importers: version: 0.3.1(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) '@langchain/community': specifier: 0.3.15 - version: 0.3.15(v4qhcw25bevfr6xzz4fnsvjiqe) + version: 0.3.15(vc5hvyy27o4cmm4jplsptc2fqm) '@langchain/core': specifier: 'catalog:' version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -542,7 +542,7 @@ importers: version: 23.0.1 langchain: specifier: 0.3.6 - version: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) + version: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) lodash: specifier: 'catalog:' version: 4.17.21 @@ -801,7 +801,7 @@ importers: version: 1.11.0 axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 bcryptjs: specifier: 2.4.3 version: 2.4.3 @@ -1120,7 +1120,7 @@ importers: dependencies: '@langchain/core': specifier: 'catalog:' - version: 0.3.19(openai@4.73.1(zod@3.23.8)) + version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) '@n8n/client-oauth2': specifier: workspace:* version: link:../@n8n/client-oauth2 @@ -1135,7 +1135,7 @@ importers: version: 1.11.0 axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 chardet: specifier: 2.0.0 version: 2.0.0 @@ -1431,7 +1431,7 @@ importers: version: 10.11.0(vue@3.5.13(typescript@5.7.2)) axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 bowser: specifier: 2.11.0 version: 2.11.0 @@ -1932,7 +1932,7 @@ importers: version: 0.15.2 axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 callsites: specifier: 3.1.0 version: 3.1.0 @@ -1978,7 +1978,7 @@ importers: devDependencies: '@langchain/core': specifier: 'catalog:' - version: 0.3.19(openai@4.73.1) + version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) '@types/deep-equal': specifier: ^1.0.1 version: 1.0.1 @@ -15666,7 +15666,7 @@ snapshots: '@gar/promisify@1.1.3': optional: true - '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i))': + '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu))': dependencies: form-data: 4.0.0 node-fetch: 2.7.0(encoding@0.1.13) @@ -15675,7 +15675,7 @@ snapshots: zod: 3.23.8 optionalDependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) - langchain: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) + langchain: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) transitivePeerDependencies: - encoding @@ -16139,7 +16139,7 @@ snapshots: - aws-crt - encoding - '@langchain/community@0.3.15(v4qhcw25bevfr6xzz4fnsvjiqe)': + '@langchain/community@0.3.15(vc5hvyy27o4cmm4jplsptc2fqm)': dependencies: '@ibm-cloud/watsonx-ai': 1.1.2 '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -16149,7 +16149,7 @@ snapshots: flat: 5.0.2 ibm-cloud-sdk-core: 5.1.0 js-yaml: 4.1.0 - langchain: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) + langchain: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) langsmith: 0.2.3(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) uuid: 10.0.0 zod: 3.23.8 @@ -16162,7 +16162,7 @@ snapshots: '@aws-sdk/client-s3': 3.666.0 '@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0) '@azure/storage-blob': 12.18.0(encoding@0.1.13) - '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i)) + '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu)) '@getzep/zep-js': 0.9.0 '@google-ai/generativelanguage': 2.6.0(encoding@0.1.13) '@google-cloud/storage': 7.12.1(encoding@0.1.13) @@ -16226,38 +16226,6 @@ snapshots: transitivePeerDependencies: - openai - '@langchain/core@0.3.19(openai@4.73.1(zod@3.23.8))': - dependencies: - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.12 - langsmith: 0.2.3(openai@4.73.1(zod@3.23.8)) - mustache: 4.2.0 - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 10.0.0 - zod: 3.23.8 - zod-to-json-schema: 3.23.3(zod@3.23.8) - transitivePeerDependencies: - - openai - - '@langchain/core@0.3.19(openai@4.73.1)': - dependencies: - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.12 - langsmith: 0.2.3(openai@4.73.1) - mustache: 4.2.0 - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 10.0.0 - zod: 3.23.8 - zod-to-json-schema: 3.23.3(zod@3.23.8) - transitivePeerDependencies: - - openai - '@langchain/google-common@0.1.3(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(zod@3.23.8)': dependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -17173,7 +17141,7 @@ snapshots: '@rudderstack/rudder-sdk-node@2.0.9(tslib@2.6.2)': dependencies: - axios: 1.7.4(debug@4.3.7) + axios: 1.7.4 axios-retry: 3.7.0 component-type: 1.2.1 join-component: 1.1.0 @@ -19470,7 +19438,7 @@ snapshots: '@babel/runtime': 7.24.7 is-retry-allowed: 2.2.0 - axios@1.7.4(debug@4.3.7): + axios@1.7.4: dependencies: follow-redirects: 1.15.6(debug@4.3.6) form-data: 4.0.0 @@ -19478,9 +19446,9 @@ snapshots: transitivePeerDependencies: - debug - axios@1.7.7: + axios@1.7.4(debug@4.3.7): dependencies: - follow-redirects: 1.15.6(debug@4.3.6) + follow-redirects: 1.15.6(debug@4.3.7) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -22347,7 +22315,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.2 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.7.4(debug@4.3.7)) + retry-axios: 2.6.0(axios@1.7.4) tough-cookie: 4.1.3 transitivePeerDependencies: - supports-color @@ -23346,7 +23314,7 @@ snapshots: kuler@2.0.0: {} - langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i): + langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu): dependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) '@langchain/openai': 0.3.14(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) @@ -23370,7 +23338,7 @@ snapshots: '@langchain/groq': 0.1.2(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) '@langchain/mistralai': 0.2.0(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))) '@langchain/ollama': 0.1.2(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))) - axios: 1.7.4(debug@4.3.7) + axios: 1.7.4 cheerio: 1.0.0 handlebars: 4.7.8 transitivePeerDependencies: @@ -23389,28 +23357,6 @@ snapshots: optionalDependencies: openai: 4.73.1(encoding@0.1.13)(zod@3.23.8) - langsmith@0.2.3(openai@4.73.1(zod@3.23.8)): - dependencies: - '@types/uuid': 10.0.0 - commander: 10.0.1 - p-queue: 6.6.2 - p-retry: 4.6.2 - semver: 7.6.0 - uuid: 10.0.0 - optionalDependencies: - openai: 4.73.1(zod@3.23.8) - - langsmith@0.2.3(openai@4.73.1): - dependencies: - '@types/uuid': 10.0.0 - commander: 10.0.1 - p-queue: 6.6.2 - p-retry: 4.6.2 - semver: 7.6.0 - uuid: 10.0.0 - optionalDependencies: - openai: 4.73.1(zod@3.23.8) - lazy-ass@1.6.0: {} ldapts@4.2.6: @@ -24743,22 +24689,6 @@ snapshots: - encoding - supports-color - openai@4.73.1(zod@3.23.8): - dependencies: - '@types/node': 18.16.16 - '@types/node-fetch': 2.6.4 - abort-controller: 3.0.0 - agentkeepalive: 4.2.1 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0(encoding@0.1.13) - optionalDependencies: - zod: 3.23.8 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - openapi-sampler@1.5.1: dependencies: '@types/json-schema': 7.0.15 @@ -25754,9 +25684,9 @@ snapshots: ret@0.1.15: {} - retry-axios@2.6.0(axios@1.7.4(debug@4.3.7)): + retry-axios@2.6.0(axios@1.7.4): dependencies: - axios: 1.7.4(debug@4.3.7) + axios: 1.7.4 retry-request@7.0.2(encoding@0.1.13): dependencies: