feat: add workflow input mapping fields to resource mapper

This commit is contained in:
Ivan Atanasov 2024-12-02 22:21:53 +01:00
parent 8c38bf2d0a
commit 2dd4096800
No known key found for this signature in database
13 changed files with 267 additions and 66 deletions

View file

@ -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,

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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';

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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,

View file

@ -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);

View file

@ -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,

View file

@ -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,
},
};

View file

@ -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,
}));

View file

@ -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);

View file

@ -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,