diff --git a/packages/cli/src/controllers/dynamic-node-parameters.controller.ts b/packages/cli/src/controllers/dynamic-node-parameters.controller.ts index f3df53d95b..a956afbbe5 100644 --- a/packages/cli/src/controllers/dynamic-node-parameters.controller.ts +++ b/packages/cli/src/controllers/dynamic-node-parameters.controller.ts @@ -93,6 +93,26 @@ export class DynamicNodeParametersController { ); } + @Post('/workflow-input-mapping-fields') + async getWorkflowInputMappingFields( + req: DynamicNodeParametersRequest.WorkflowInputMappingFields, + ) { + const { path, methodName, credentials, currentNodeParameters, nodeTypeAndVersion } = req.body; + + if (!methodName) throw new BadRequestError('Missing `methodName` in request body'); + + const additionalData = await getBase(req.user.id, currentNodeParameters); + + return await this.service.getWorkflowInputMappingFields( + methodName, + path, + additionalData, + nodeTypeAndVersion, + currentNodeParameters, + credentials, + ); + } + @Post('/action-result') async getActionResult( req: DynamicNodeParametersRequest.ActionResult, diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index f233d7db46..e8ee911399 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -385,6 +385,11 @@ export declare namespace DynamicNodeParametersRequest { methodName: string; }>; + /** POST dynamic-node-parameters/workflow-input-mapping-fields */ + type WorkflowInputMappingFields = BaseRequest<{ + methodName: string; + }>; + /** POST /dynamic-node-parameters/action-result */ type ActionResult = BaseRequest<{ handler: string; diff --git a/packages/cli/src/services/dynamic-node-parameters.service.ts b/packages/cli/src/services/dynamic-node-parameters.service.ts index eb6ecc5f67..bc91883c57 100644 --- a/packages/cli/src/services/dynamic-node-parameters.service.ts +++ b/packages/cli/src/services/dynamic-node-parameters.service.ts @@ -1,4 +1,4 @@ -import { LoadOptionsContext, NodeExecuteFunctions } from 'n8n-core'; +import { LoadOptionsContext, NodeExecuteFunctions, WorkflowInputsContext } from 'n8n-core'; import type { ILoadOptions, ILoadOptionsFunctions, @@ -17,12 +17,35 @@ import type { INodeTypeNameVersion, NodeParameterValueType, IDataObject, + IWorkflowInputsLoadOptionsFunctions, } from 'n8n-workflow'; import { Workflow, RoutingNode, ApplicationError } from 'n8n-workflow'; import { Service } from 'typedi'; import { NodeTypes } from '@/node-types'; +type WorkflowInputsMappingMethod = ( + this: IWorkflowInputsLoadOptionsFunctions, +) => Promise; +type ListSearchMethod = ( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +) => Promise; +type LoadOptionsMethod = (this: ILoadOptionsFunctions) => Promise; +type ActionHandlerMethod = ( + this: ILoadOptionsFunctions, + payload?: string, +) => Promise; +type ResourceMappingMethod = (this: ILoadOptionsFunctions) => Promise; + +type NodeMethod = + | WorkflowInputsMappingMethod + | ListSearchMethod + | LoadOptionsMethod + | ActionHandlerMethod + | ResourceMappingMethod; + @Service() export class DynamicNodeParametersService { constructor(private nodeTypes: NodeTypes) {} @@ -159,6 +182,23 @@ export class DynamicNodeParametersService { return method.call(thisArgs); } + /** Returns the available workflow input mapping fields for the ResourceMapper component */ + async getWorkflowInputMappingFields( + methodName: string, + path: string, + additionalData: IWorkflowExecuteAdditionalData, + nodeTypeAndVersion: INodeTypeNameVersion, + currentNodeParameters: INodeParameters, + credentials?: INodeCredentials, + ): Promise { + const nodeType = this.getNodeType(nodeTypeAndVersion); + const method = this.getMethod('workflowInputsMapping', methodName, nodeType); + const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials); + const thisArgs = this.getWorkflowInputsContext(path, additionalData, workflow); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return method.call(thisArgs); + } + /** Returns the result of the action handler */ async getActionResult( handler: string, @@ -181,33 +221,34 @@ export class DynamicNodeParametersService { type: 'resourceMapping', methodName: string, nodeType: INodeType, - ): (this: ILoadOptionsFunctions) => Promise; + ): ResourceMappingMethod; private getMethod( - type: 'listSearch', + type: 'workflowInputsMapping', methodName: string, nodeType: INodeType, - ): ( - this: ILoadOptionsFunctions, - filter?: string | undefined, - paginationToken?: string | undefined, - ) => Promise; + ): WorkflowInputsMappingMethod; + private getMethod(type: 'listSearch', methodName: string, nodeType: INodeType): ListSearchMethod; private getMethod( type: 'loadOptions', methodName: string, nodeType: INodeType, - ): (this: ILoadOptionsFunctions) => Promise; + ): LoadOptionsMethod; private getMethod( type: 'actionHandler', methodName: string, nodeType: INodeType, - ): (this: ILoadOptionsFunctions, payload?: string) => Promise; - + ): ActionHandlerMethod; private getMethod( - type: 'resourceMapping' | 'listSearch' | 'loadOptions' | 'actionHandler', + type: + | 'resourceMapping' + | 'workflowInputsMapping' + | 'listSearch' + | 'loadOptions' + | 'actionHandler', methodName: string, nodeType: INodeType, - ) { - const method = nodeType.methods?.[type]?.[methodName]; + ): NodeMethod { + const method = nodeType.methods?.[type]?.[methodName] as NodeMethod; if (typeof method !== 'function') { throw new ApplicationError('Node type does not have method defined', { tags: { nodeType: nodeType.description.name }, @@ -255,4 +296,13 @@ export class DynamicNodeParametersService { const node = workflow.nodes['Temp-Node']; return new LoadOptionsContext(workflow, node, additionalData, path); } + + private getWorkflowInputsContext( + path: string, + additionalData: IWorkflowExecuteAdditionalData, + workflow: Workflow, + ) { + const node = workflow.nodes['Temp-Node']; + return new WorkflowInputsContext(workflow, node, additionalData, path); + } } diff --git a/packages/core/src/node-execution-context/index.ts b/packages/core/src/node-execution-context/index.ts index 00c90266db..57f50193f0 100644 --- a/packages/core/src/node-execution-context/index.ts +++ b/packages/core/src/node-execution-context/index.ts @@ -3,6 +3,7 @@ export { ExecuteContext } from './execute-context'; export { ExecuteSingleContext } from './execute-single-context'; export { HookContext } from './hook-context'; export { LoadOptionsContext } from './load-options-context'; +export { WorkflowInputsContext } from './workflow-inputs-context'; export { PollContext } from './poll-context'; export { SupplyDataContext } from './supply-data-context'; export { TriggerContext } from './trigger-context'; diff --git a/packages/core/src/node-execution-context/workflow-inputs-context.ts b/packages/core/src/node-execution-context/workflow-inputs-context.ts new file mode 100644 index 0000000000..2088200605 --- /dev/null +++ b/packages/core/src/node-execution-context/workflow-inputs-context.ts @@ -0,0 +1,78 @@ +import { get } from 'lodash'; +import { ApplicationError } from 'n8n-workflow'; +import type { + INodeParameterResourceLocator, + IGetNodeParameterOptions, + INode, + IWorkflowExecuteAdditionalData, + NodeParameterValueType, + Workflow, + IWorkflowInputsLoadOptionsFunctions, + FieldType, +} from 'n8n-workflow'; + +import { extractValue } from '@/ExtractValue'; + +import { NodeExecutionContext } from './node-execution-context'; +// eslint-disable-next-line import/no-cycle + +export class WorkflowInputsContext + extends NodeExecutionContext + implements IWorkflowInputsLoadOptionsFunctions +{ + constructor( + workflow: Workflow, + node: INode, + additionalData: IWorkflowExecuteAdditionalData, + private readonly path: string, + ) { + super(workflow, node, additionalData, 'internal'); + } + + getWorkflowInputValues(): Array<{ name: string; type: FieldType }> { + const { value } = this.getCurrentNodeParameter('workflowId') as INodeParameterResourceLocator; + + const workflowId = value as string; + if (!workflowId) { + throw new ApplicationError('No workflowId defined on node!'); + } + + // TODO: load the inputs from the workflow + const dummyFields = [ + { name: 'field1', type: 'string' as const }, + { name: 'field2', type: 'number' as const }, + { name: 'field3', type: 'boolean' as const }, + ]; + + return dummyFields; + } + + getCurrentNodeParameter( + parameterPath: string, + options?: IGetNodeParameterOptions, + ): NodeParameterValueType | object | undefined { + const nodeParameters = this.additionalData.currentNodeParameters; + + if (parameterPath.charAt(0) === '&') { + parameterPath = `${this.path.split('.').slice(1, -1).join('.')}.${parameterPath.slice(1)}`; + } + + let returnData = get(nodeParameters, parameterPath); + + // This is outside the try/catch because it throws errors with proper messages + if (options?.extractValue) { + const nodeType = this.workflow.nodeTypes.getByNameAndVersion( + this.node.type, + this.node.typeVersion, + ); + returnData = extractValue( + returnData, + parameterPath, + this.node, + nodeType, + ) as NodeParameterValueType; + } + + return returnData; + } +} diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 20b7079b6f..ce570c8dcf 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1275,6 +1275,10 @@ export declare namespace DynamicNodeParameters { methodName: string; } + interface WorkflowInputMappingFieldsRequest extends BaseRequest { + methodName: string; + } + interface ActionResultRequest extends BaseRequest { handler: string; payload: IDataObject | string | undefined; diff --git a/packages/editor-ui/src/api/nodeTypes.ts b/packages/editor-ui/src/api/nodeTypes.ts index f4d516aaef..98584018d3 100644 --- a/packages/editor-ui/src/api/nodeTypes.ts +++ b/packages/editor-ui/src/api/nodeTypes.ts @@ -59,6 +59,18 @@ export async function getResourceMapperFields( ); } +export async function getWorkflowInputFields( + context: IRestApiContext, + sendData: DynamicNodeParameters.WorkflowInputMappingFieldsRequest, +): Promise { + return await makeRestApiRequest( + context, + 'POST', + '/dynamic-node-parameters/workflow-input-mapping-fields', + sendData, + ); +} + export async function getNodeParameterActionResult( context: IRestApiContext, sendData: DynamicNodeParameters.ActionResultRequest, diff --git a/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue b/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue index b09354ed00..b61e109c73 100644 --- a/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue +++ b/packages/editor-ui/src/components/ResourceMapper/ResourceMapper.vue @@ -244,25 +244,45 @@ async function loadFieldsToMap(): Promise { return; } - const methodName = props.parameter.typeOptions?.resourceMapper?.resourceMapperMethod; - if (typeof methodName !== 'string') { - return; + const resourceMapperMethod = props.parameter.typeOptions?.resourceMapper?.resourceMapperMethod; + const workflowInputsMappingMethod = + props.parameter.typeOptions?.resourceMapper?.workflowInputsMappingMethod; + + let fetchedFields = null; + + if (typeof resourceMapperMethod === 'string') { + const requestParams: DynamicNodeParameters.ResourceMapperFieldsRequest = { + nodeTypeAndVersion: { + name: props.node?.type, + version: props.node.typeVersion, + }, + currentNodeParameters: resolveRequiredParameters( + props.parameter, + props.node.parameters, + ) as INodeParameters, + path: props.path, + methodName: resourceMapperMethod, + credentials: props.node.credentials, + }; + fetchedFields = await nodeTypesStore.getResourceMapperFields(requestParams); + } else { + if (typeof workflowInputsMappingMethod === 'string') { + const requestParams: DynamicNodeParameters.WorkflowInputMappingFieldsRequest = { + nodeTypeAndVersion: { + name: props.node?.type, + version: props.node.typeVersion, + }, + currentNodeParameters: resolveRequiredParameters( + props.parameter, + props.node.parameters, + ) as INodeParameters, + path: props.path, + methodName: workflowInputsMappingMethod, + }; + fetchedFields = await nodeTypesStore.getWorkflowInputFields(requestParams); + } } - const requestParams: DynamicNodeParameters.ResourceMapperFieldsRequest = { - nodeTypeAndVersion: { - name: props.node?.type, - version: props.node.typeVersion, - }, - currentNodeParameters: resolveRequiredParameters( - props.parameter, - props.node.parameters, - ) as INodeParameters, - path: props.path, - methodName, - credentials: props.node.credentials, - }; - const fetchedFields = await nodeTypesStore.getResourceMapperFields(requestParams); if (fetchedFields !== null) { const newSchema = fetchedFields.fields.map((field) => { const existingField = state.paramValue.schema.find((f) => f.id === field.id); diff --git a/packages/editor-ui/src/stores/nodeTypes.store.ts b/packages/editor-ui/src/stores/nodeTypes.store.ts index 7a8e66aab0..f49704d64e 100644 --- a/packages/editor-ui/src/stores/nodeTypes.store.ts +++ b/packages/editor-ui/src/stores/nodeTypes.store.ts @@ -302,6 +302,16 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => { } }; + const getWorkflowInputFields = async ( + sendData: DynamicNodeParameters.WorkflowInputMappingFieldsRequest, + ) => { + try { + return await nodeTypesApi.getWorkflowInputFields(rootStore.restApiContext, sendData); + } catch (error) { + return null; + } + }; + const getNodeParameterActionResult = async ( sendData: DynamicNodeParameters.ActionResultRequest, ) => { @@ -326,6 +336,7 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => { visibleNodeTypesByInputConnectionTypeNames, isConfigurableNode, getResourceMapperFields, + getWorkflowInputFields, getNodeParameterActionResult, getResourceLocatorResults, getNodeParameterOptions, diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow.node.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow.node.ts index 6346460bd9..f8de2e25b7 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow.node.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow.node.ts @@ -199,9 +199,10 @@ export class ExecuteWorkflow implements INodeType { }, required: true, typeOptions: { - loadOptionsDependsOn: ['workflowId.value', 'workflowJson'], + loadOptionsDependsOn: ['workflowId.value'], resourceMapper: { - resourceMapperMethod: 'getWorkflowInputs', + workflowInputsMappingMethod: 'getWorkflowInputs', + valuesLabel: 'Workflow Inputs', mode: 'add', fieldWords: { singular: 'workflow input', @@ -214,11 +215,11 @@ export class ExecuteWorkflow implements INodeType { }, displayOptions: { show: { + source: ['database'], '@version': [{ _cnd: { gte: 1.2 } }], }, hide: { workflowId: [''], - workflowJson: ['\n\n\n'], }, }, }, @@ -264,7 +265,7 @@ export class ExecuteWorkflow implements INodeType { }; methods = { - resourceMapping: { + workflowInputsMapping: { getWorkflowInputs, }, }; diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/methods/resourceMapping.ts b/packages/nodes-base/nodes/ExecuteWorkflow/methods/resourceMapping.ts index 157457fb86..979b333b2d 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow/methods/resourceMapping.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow/methods/resourceMapping.ts @@ -1,34 +1,14 @@ import type { FieldType, - ILoadOptionsFunctions, + IWorkflowInputsLoadOptionsFunctions, ResourceMapperField, ResourceMapperFields, } from 'n8n-workflow'; -import { getWorkflowInfo } from '../GenericFunctions'; - export async function getWorkflowInputs( - this: ILoadOptionsFunctions, + this: IWorkflowInputsLoadOptionsFunctions, ): Promise { - const source = this.getNodeParameter('source', 0) as string; - - const executeWorkflowInfo = await getWorkflowInfo.call(this, source); - - if (executeWorkflowInfo.code === undefined) { - // executeWorkflowInfo.code = await getWorkflowById.call(this, executeWorkflowInfo.id as string); - } - - const workflowInputs = ( - Array.isArray( - executeWorkflowInfo.code?.nodes.find( - (node) => node.type === 'n8n-nodes-base.executeWorkflowTrigger', - )?.parameters.workflowInputs, - ) - ? executeWorkflowInfo.code?.nodes.find( - (node) => node.type === 'n8n-nodes-base.executeWorkflowTrigger', - )?.parameters.workflowInputs - : [] - ) as Array<{ name: string; type: FieldType }>; + const workflowInputs = this.getWorkflowInputValues() as Array<{ name: string; type: FieldType }>; const fields: ResourceMapperField[] = workflowInputs.map((currentWorkflowInput) => ({ id: currentWorkflowInput.name, @@ -36,7 +16,7 @@ export async function getWorkflowInputs( required: false, defaultMatch: true, display: true, - type: currentWorkflowInput.type || 'string', + type: currentWorkflowInput.type, canBeUsedToMatch: true, })); diff --git a/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts b/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts index 726ab0b948..92609155a9 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.ts @@ -95,10 +95,7 @@ function getFieldEntries(context: IExecuteFunctions): ValueOptions[] { let result: ValueOptions[] | string = 'Internal Error: Invalid input source'; try { if (inputSource === WORKFLOW_INPUTS) { - result = context.getNodeParameter(`${WORKFLOW_INPUTS}.${VALUES}`, 0, []) as Array<{ - name: string; - type: FieldType; - }>; + result = context.getNodeParameter(`${WORKFLOW_INPUTS}.${VALUES}`, 0, []) as ValueOptions[]; } else if (inputSource === JSON_EXAMPLE) { const schema = parseJsonExample(context); result = parseJsonSchema(schema); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 0a7a5d0379..79f71cb7f2 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1070,6 +1070,10 @@ export interface ILoadOptionsFunctions extends FunctionsBase { helpers: RequestHelperFunctions & SSHTunnelFunctions; } +export interface IWorkflowInputsLoadOptionsFunctions { + getWorkflowInputValues(): Array<{ name: string; type: FieldType }>; +} + export interface IPollFunctions extends FunctionsBaseWithRequiredKeys<'getMode' | 'getActivationMode'> { __emit( @@ -1350,11 +1354,13 @@ export interface INodePropertyTypeOptions { [key: string]: any; } -export interface ResourceMapperTypeOptions { - resourceMapperMethod: string; +export interface ResourceMapperTypeOptionsBase { mode: 'add' | 'update' | 'upsert'; valuesLabel?: string; - fieldWords?: { singular: string; plural: string }; + fieldWords?: { + singular: string; + plural: string; + }; addAllFields?: boolean; noFieldsError?: string; multiKeyMatch?: boolean; @@ -1366,6 +1372,17 @@ export interface ResourceMapperTypeOptions { }; } +// Enforce at least one of resourceMapperMethod or workflowInputsMappingMethod +export type ResourceMapperTypeOptions = + | (ResourceMapperTypeOptionsBase & { + resourceMapperMethod: string; + workflowInputsMappingMethod?: never; + }) + | (ResourceMapperTypeOptionsBase & { + workflowInputsMappingMethod: string; + resourceMapperMethod?: never; + }); + type NonEmptyArray = [T, ...T[]]; export type FilterTypeCombinator = 'and' | 'or'; @@ -1628,6 +1645,11 @@ export interface INodeType { resourceMapping?: { [functionName: string]: (this: ILoadOptionsFunctions) => Promise; }; + workflowInputsMapping?: { + [functionName: string]: ( + this: IWorkflowInputsLoadOptionsFunctions, + ) => Promise; + }; actionHandler?: { [functionName: string]: ( this: ILoadOptionsFunctions,