mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat: add workflow input mapping fields to resource mapper
This commit is contained in:
parent
8c38bf2d0a
commit
2dd4096800
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<ResourceMapperFields>;
|
||||
type ListSearchMethod = (
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
) => Promise<INodeListSearchResult>;
|
||||
type LoadOptionsMethod = (this: ILoadOptionsFunctions) => Promise<INodePropertyOptions[]>;
|
||||
type ActionHandlerMethod = (
|
||||
this: ILoadOptionsFunctions,
|
||||
payload?: string,
|
||||
) => Promise<NodeParameterValueType>;
|
||||
type ResourceMappingMethod = (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
||||
|
||||
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<ResourceMapperFields> {
|
||||
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<ResourceMapperFields>;
|
||||
): ResourceMappingMethod;
|
||||
private getMethod(
|
||||
type: 'listSearch',
|
||||
type: 'workflowInputsMapping',
|
||||
methodName: string,
|
||||
nodeType: INodeType,
|
||||
): (
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string | undefined,
|
||||
paginationToken?: string | undefined,
|
||||
) => Promise<INodeListSearchResult>;
|
||||
): WorkflowInputsMappingMethod;
|
||||
private getMethod(type: 'listSearch', methodName: string, nodeType: INodeType): ListSearchMethod;
|
||||
private getMethod(
|
||||
type: 'loadOptions',
|
||||
methodName: string,
|
||||
nodeType: INodeType,
|
||||
): (this: ILoadOptionsFunctions) => Promise<INodePropertyOptions[]>;
|
||||
): LoadOptionsMethod;
|
||||
private getMethod(
|
||||
type: 'actionHandler',
|
||||
methodName: string,
|
||||
nodeType: INodeType,
|
||||
): (this: ILoadOptionsFunctions, payload?: string) => Promise<NodeParameterValueType>;
|
||||
|
||||
): 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -59,6 +59,18 @@ export async function getResourceMapperFields(
|
|||
);
|
||||
}
|
||||
|
||||
export async function getWorkflowInputFields(
|
||||
context: IRestApiContext,
|
||||
sendData: DynamicNodeParameters.WorkflowInputMappingFieldsRequest,
|
||||
): Promise<ResourceMapperFields> {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'POST',
|
||||
'/dynamic-node-parameters/workflow-input-mapping-fields',
|
||||
sendData,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getNodeParameterActionResult(
|
||||
context: IRestApiContext,
|
||||
sendData: DynamicNodeParameters.ActionResultRequest,
|
||||
|
|
|
@ -244,11 +244,13 @@ async function loadFieldsToMap(): Promise<void> {
|
|||
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,
|
||||
|
@ -259,10 +261,28 @@ async function loadFieldsToMap(): Promise<void> {
|
|||
props.node.parameters,
|
||||
) as INodeParameters,
|
||||
path: props.path,
|
||||
methodName,
|
||||
methodName: resourceMapperMethod,
|
||||
credentials: props.node.credentials,
|
||||
};
|
||||
const fetchedFields = await nodeTypesStore.getResourceMapperFields(requestParams);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (fetchedFields !== null) {
|
||||
const newSchema = fetchedFields.fields.map((field) => {
|
||||
const existingField = state.paramValue.schema.find((f) => f.id === field.id);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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<ResourceMapperFields> {
|
||||
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,
|
||||
}));
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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, ...T[]];
|
||||
|
||||
export type FilterTypeCombinator = 'and' | 'or';
|
||||
|
@ -1628,6 +1645,11 @@ export interface INodeType {
|
|||
resourceMapping?: {
|
||||
[functionName: string]: (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
||||
};
|
||||
workflowInputsMapping?: {
|
||||
[functionName: string]: (
|
||||
this: IWorkflowInputsLoadOptionsFunctions,
|
||||
) => Promise<ResourceMapperFields>;
|
||||
};
|
||||
actionHandler?: {
|
||||
[functionName: string]: (
|
||||
this: ILoadOptionsFunctions,
|
||||
|
|
Loading…
Reference in a new issue