mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
Ado-2898-execute-workflow-node-add-workflow-inputs-parameter (#11887)
Co-authored-by: Charlie Kolb <charlie@n8n.io>
This commit is contained in:
parent
8e374b4394
commit
c31bf0a4d1
|
@ -93,6 +93,22 @@ export class DynamicNodeParametersController {
|
|||
);
|
||||
}
|
||||
|
||||
@Post('/local-resource-mapper-fields')
|
||||
async getLocalResourceMappingFields(req: DynamicNodeParametersRequest.ResourceMapperFields) {
|
||||
const { path, methodName, 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.getLocalResourceMappingFields(
|
||||
methodName,
|
||||
path,
|
||||
additionalData,
|
||||
nodeTypeAndVersion,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/action-result')
|
||||
async getActionResult(
|
||||
req: DynamicNodeParametersRequest.ActionResult,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { LoadOptionsContext, NodeExecuteFunctions } from 'n8n-core';
|
||||
import { LoadOptionsContext, NodeExecuteFunctions, LocalLoadOptionsContext } from 'n8n-core';
|
||||
import type {
|
||||
ILoadOptions,
|
||||
ILoadOptionsFunctions,
|
||||
|
@ -17,15 +17,43 @@ import type {
|
|||
INodeTypeNameVersion,
|
||||
NodeParameterValueType,
|
||||
IDataObject,
|
||||
ILocalLoadOptionsFunctions,
|
||||
} from 'n8n-workflow';
|
||||
import { Workflow, RoutingNode, ApplicationError } from 'n8n-workflow';
|
||||
import { Service } from 'typedi';
|
||||
|
||||
import { NodeTypes } from '@/node-types';
|
||||
|
||||
import { WorkflowLoaderService } from './workflow-loader.service';
|
||||
|
||||
type LocalResourceMappingMethod = (
|
||||
this: ILocalLoadOptionsFunctions,
|
||||
) => 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 =
|
||||
| LocalResourceMappingMethod
|
||||
| ListSearchMethod
|
||||
| LoadOptionsMethod
|
||||
| ActionHandlerMethod
|
||||
| ResourceMappingMethod;
|
||||
|
||||
@Service()
|
||||
export class DynamicNodeParametersService {
|
||||
constructor(private nodeTypes: NodeTypes) {}
|
||||
constructor(
|
||||
private nodeTypes: NodeTypes,
|
||||
private workflowLoaderService: WorkflowLoaderService,
|
||||
) {}
|
||||
|
||||
/** Returns the available options via a predefined method */
|
||||
async getOptionsViaMethodName(
|
||||
|
@ -159,6 +187,20 @@ export class DynamicNodeParametersService {
|
|||
return method.call(thisArgs);
|
||||
}
|
||||
|
||||
/** Returns the available workflow input mapping fields for the ResourceMapper component */
|
||||
async getLocalResourceMappingFields(
|
||||
methodName: string,
|
||||
path: string,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
nodeTypeAndVersion: INodeTypeNameVersion,
|
||||
): Promise<ResourceMapperFields> {
|
||||
const nodeType = this.getNodeType(nodeTypeAndVersion);
|
||||
const method = this.getMethod('localResourceMapping', methodName, nodeType);
|
||||
const thisArgs = this.getLocalLoadOptionsContext(path, additionalData);
|
||||
// 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 +223,34 @@ export class DynamicNodeParametersService {
|
|||
type: 'resourceMapping',
|
||||
methodName: string,
|
||||
nodeType: INodeType,
|
||||
): (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
||||
): ResourceMappingMethod;
|
||||
private getMethod(
|
||||
type: 'listSearch',
|
||||
type: 'localResourceMapping',
|
||||
methodName: string,
|
||||
nodeType: INodeType,
|
||||
): (
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string | undefined,
|
||||
paginationToken?: string | undefined,
|
||||
) => Promise<INodeListSearchResult>;
|
||||
): LocalResourceMappingMethod;
|
||||
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'
|
||||
| 'localResourceMapping'
|
||||
| '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 +298,16 @@ export class DynamicNodeParametersService {
|
|||
const node = workflow.nodes['Temp-Node'];
|
||||
return new LoadOptionsContext(workflow, node, additionalData, path);
|
||||
}
|
||||
|
||||
private getLocalLoadOptionsContext(
|
||||
path: string,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
): ILocalLoadOptionsFunctions {
|
||||
return new LocalLoadOptionsContext(
|
||||
this.nodeTypes,
|
||||
additionalData,
|
||||
path,
|
||||
this.workflowLoaderService,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
19
packages/cli/src/services/workflow-loader.service.ts
Normal file
19
packages/cli/src/services/workflow-loader.service.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { ApplicationError, type IWorkflowBase, type IWorkflowLoader } from 'n8n-workflow';
|
||||
import { Service } from 'typedi';
|
||||
|
||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||
|
||||
@Service()
|
||||
export class WorkflowLoaderService implements IWorkflowLoader {
|
||||
constructor(private readonly workflowRepository: WorkflowRepository) {}
|
||||
|
||||
async get(workflowId: string): Promise<IWorkflowBase> {
|
||||
const workflow = await this.workflowRepository.findById(workflowId);
|
||||
|
||||
if (!workflow) {
|
||||
throw new ApplicationError(`Failed to find workflow with ID "${workflowId}"`);
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
||||
}
|
|
@ -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 { LocalLoadOptionsContext } from './local-load-options-context';
|
||||
export { PollContext } from './poll-context';
|
||||
export { SupplyDataContext } from './supply-data-context';
|
||||
export { TriggerContext } from './trigger-context';
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import { get } from 'lodash';
|
||||
import { ApplicationError, Workflow } from 'n8n-workflow';
|
||||
import type {
|
||||
INodeParameterResourceLocator,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeParameterValueType,
|
||||
ILocalLoadOptionsFunctions,
|
||||
IWorkflowLoader,
|
||||
IWorkflowNodeContext,
|
||||
INode,
|
||||
INodeTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { LoadWorkflowNodeContext } from './workflow-node-context';
|
||||
|
||||
export class LocalLoadOptionsContext implements ILocalLoadOptionsFunctions {
|
||||
constructor(
|
||||
private nodeTypes: INodeTypes,
|
||||
private additionalData: IWorkflowExecuteAdditionalData,
|
||||
private path: string,
|
||||
private workflowLoader: IWorkflowLoader,
|
||||
) {}
|
||||
|
||||
async getWorkflowNodeContext(nodeType: string): Promise<IWorkflowNodeContext | null> {
|
||||
const { value } = this.getCurrentNodeParameter('workflowId') as INodeParameterResourceLocator;
|
||||
|
||||
const workflowId = value as string;
|
||||
if (!workflowId) {
|
||||
throw new ApplicationError(`No workflowId parameter defined on node of type "${nodeType}"!`);
|
||||
}
|
||||
|
||||
const dbWorkflow = await this.workflowLoader.get(workflowId);
|
||||
|
||||
const selectedWorkflowNode = dbWorkflow.nodes.find((node) => node.type === nodeType) as INode;
|
||||
|
||||
if (selectedWorkflowNode) {
|
||||
const selectedSingleNodeWorkflow = new Workflow({
|
||||
nodes: [selectedWorkflowNode],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes: this.nodeTypes,
|
||||
});
|
||||
|
||||
const workflowAdditionalData = { ...this.additionalData };
|
||||
workflowAdditionalData.currentNodeParameters = selectedWorkflowNode.parameters;
|
||||
|
||||
return new LoadWorkflowNodeContext(
|
||||
selectedSingleNodeWorkflow,
|
||||
selectedWorkflowNode,
|
||||
workflowAdditionalData,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getCurrentNodeParameter(parameterPath: string): NodeParameterValueType | object | undefined {
|
||||
const nodeParameters = this.additionalData.currentNodeParameters;
|
||||
|
||||
if (parameterPath.startsWith('&')) {
|
||||
parameterPath = `${this.path.split('.').slice(1, -1).join('.')}.${parameterPath.slice(1)}`;
|
||||
}
|
||||
|
||||
const returnData = get(nodeParameters, parameterPath);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import type {
|
||||
IGetNodeParameterOptions,
|
||||
INode,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
Workflow,
|
||||
IWorkflowNodeContext,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { NodeExecutionContext } from './node-execution-context';
|
||||
|
||||
export class LoadWorkflowNodeContext extends NodeExecutionContext implements IWorkflowNodeContext {
|
||||
readonly getNodeParameter: IWorkflowNodeContext['getNodeParameter'];
|
||||
|
||||
constructor(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData) {
|
||||
super(workflow, node, additionalData, 'internal');
|
||||
{
|
||||
this.getNodeParameter = ((
|
||||
parameterName: string,
|
||||
itemIndex: number,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fallbackValue?: any,
|
||||
options?: IGetNodeParameterOptions,
|
||||
) =>
|
||||
this._getNodeParameter(
|
||||
parameterName,
|
||||
itemIndex,
|
||||
fallbackValue,
|
||||
options,
|
||||
)) as IWorkflowNodeContext['getNodeParameter'];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -59,6 +59,18 @@ export async function getResourceMapperFields(
|
|||
);
|
||||
}
|
||||
|
||||
export async function getLocalResourceMapperFields(
|
||||
context: IRestApiContext,
|
||||
sendData: DynamicNodeParameters.ResourceMapperFieldsRequest,
|
||||
): Promise<ResourceMapperFields> {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'POST',
|
||||
'/dynamic-node-parameters/local-resource-mapper-fields',
|
||||
sendData,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getNodeParameterActionResult(
|
||||
context: IRestApiContext,
|
||||
sendData: DynamicNodeParameters.ActionResultRequest,
|
||||
|
|
|
@ -239,20 +239,14 @@ async function initFetching(inlineLoading = false): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
async function loadFieldsToMap(): Promise<void> {
|
||||
const createRequestParams = (methodName: string) => {
|
||||
if (!props.node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const methodName = props.parameter.typeOptions?.resourceMapper?.resourceMapperMethod;
|
||||
if (typeof methodName !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestParams: DynamicNodeParameters.ResourceMapperFieldsRequest = {
|
||||
nodeTypeAndVersion: {
|
||||
name: props.node?.type,
|
||||
version: props.node.typeVersion,
|
||||
version: props.node?.typeVersion,
|
||||
},
|
||||
currentNodeParameters: resolveRequiredParameters(
|
||||
props.parameter,
|
||||
|
@ -262,7 +256,33 @@ async function loadFieldsToMap(): Promise<void> {
|
|||
methodName,
|
||||
credentials: props.node.credentials,
|
||||
};
|
||||
const fetchedFields = await nodeTypesStore.getResourceMapperFields(requestParams);
|
||||
|
||||
return requestParams;
|
||||
};
|
||||
|
||||
async function loadFieldsToMap(): Promise<void> {
|
||||
if (!props.node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { resourceMapperMethod, localResourceMapperMethod } =
|
||||
props.parameter.typeOptions?.resourceMapper ?? {};
|
||||
|
||||
let fetchedFields = null;
|
||||
|
||||
if (typeof resourceMapperMethod === 'string') {
|
||||
const requestParams = createRequestParams(
|
||||
resourceMapperMethod,
|
||||
) as DynamicNodeParameters.ResourceMapperFieldsRequest;
|
||||
fetchedFields = await nodeTypesStore.getResourceMapperFields(requestParams);
|
||||
} else if (typeof localResourceMapperMethod === 'string') {
|
||||
const requestParams = createRequestParams(
|
||||
localResourceMapperMethod,
|
||||
) as DynamicNodeParameters.ResourceMapperFieldsRequest;
|
||||
|
||||
fetchedFields = await nodeTypesStore.getLocalResourceMapperFields(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 getLocalResourceMapperFields = async (
|
||||
sendData: DynamicNodeParameters.ResourceMapperFieldsRequest,
|
||||
) => {
|
||||
try {
|
||||
return await nodeTypesApi.getLocalResourceMapperFields(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,
|
||||
getLocalResourceMapperFields,
|
||||
getNodeParameterActionResult,
|
||||
getResourceLocatorResults,
|
||||
getNodeParameterOptions,
|
||||
|
|
|
@ -1,14 +1,34 @@
|
|||
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
||||
import type {
|
||||
ExecuteWorkflowData,
|
||||
FieldValueOption,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
ResourceMapperField,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { getWorkflowInfo } from './GenericFunctions';
|
||||
import { generatePairedItemData } from '../../utils/utilities';
|
||||
import { loadWorkflowInputMappings } from './methods/resourceMapping';
|
||||
import { generatePairedItemData } from '../../../utils/utilities';
|
||||
import { getWorkflowInputData } from '../GenericFunctions';
|
||||
|
||||
function getCurrentWorkflowInputData(this: IExecuteFunctions) {
|
||||
const inputData = this.getInputData();
|
||||
|
||||
if (this.getNode().typeVersion < 1.2) {
|
||||
return inputData;
|
||||
} else {
|
||||
const schema = this.getNodeParameter('workflowInputs.schema', 0, []) as ResourceMapperField[];
|
||||
const newParams = schema
|
||||
.filter((x) => !x.removed)
|
||||
.map((x) => ({ name: x.displayName, type: x.type ?? 'any' })) as FieldValueOption[];
|
||||
|
||||
// TODO: map every row to field values so we can set the static value or expression later on
|
||||
return getWorkflowInputData.call(this, inputData, newParams);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExecuteWorkflow implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
|
@ -187,6 +207,41 @@ export class ExecuteWorkflow implements INodeType {
|
|||
default: '',
|
||||
displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } },
|
||||
},
|
||||
{
|
||||
displayName: 'Workflow Inputs',
|
||||
name: 'workflowInputs',
|
||||
type: 'resourceMapper',
|
||||
noDataExpression: true,
|
||||
default: {
|
||||
mappingMode: 'defineBelow',
|
||||
value: null,
|
||||
},
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['workflowId.value'],
|
||||
resourceMapper: {
|
||||
localResourceMapperMethod: 'loadWorkflowInputMappings',
|
||||
valuesLabel: 'Workflow Inputs',
|
||||
mode: 'add',
|
||||
fieldWords: {
|
||||
singular: 'workflow input',
|
||||
plural: 'workflow inputs',
|
||||
},
|
||||
addAllFields: true,
|
||||
multiKeyMatch: false,
|
||||
supportAutoMap: false,
|
||||
},
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
source: ['database'],
|
||||
'@version': [{ _cnd: { gte: 1.2 } }],
|
||||
},
|
||||
hide: {
|
||||
workflowId: [''],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Mode',
|
||||
name: 'mode',
|
||||
|
@ -228,10 +283,16 @@ export class ExecuteWorkflow implements INodeType {
|
|||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
localResourceMapping: {
|
||||
loadWorkflowInputMappings,
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const source = this.getNodeParameter('source', 0) as string;
|
||||
const mode = this.getNodeParameter('mode', 0, false) as string;
|
||||
const items = this.getInputData();
|
||||
const items = getCurrentWorkflowInputData.call(this);
|
||||
|
||||
const workflowProxy = this.getWorkflowDataProxy(0);
|
||||
const currentWorkflowId = workflowProxy.$workflow.id as string;
|
|
@ -0,0 +1,72 @@
|
|||
import { readFile as fsReadFile } from 'fs/promises';
|
||||
import { NodeOperationError, jsonParse } from 'n8n-workflow';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IExecuteWorkflowInfo,
|
||||
ILoadOptionsFunctions,
|
||||
INodeParameterResourceLocator,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function getWorkflowInfo(
|
||||
this: ILoadOptionsFunctions | IExecuteFunctions,
|
||||
source: string,
|
||||
itemIndex = 0,
|
||||
) {
|
||||
const workflowInfo: IExecuteWorkflowInfo = {};
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
if (source === 'database') {
|
||||
// Read workflow from database
|
||||
if (nodeVersion === 1) {
|
||||
workflowInfo.id = this.getNodeParameter('workflowId', itemIndex) as string;
|
||||
} else {
|
||||
const { value } = this.getNodeParameter(
|
||||
'workflowId',
|
||||
itemIndex,
|
||||
{},
|
||||
) as INodeParameterResourceLocator;
|
||||
workflowInfo.id = value as string;
|
||||
}
|
||||
} else if (source === 'localFile') {
|
||||
// Read workflow from filesystem
|
||||
const workflowPath = this.getNodeParameter('workflowPath', itemIndex) as string;
|
||||
|
||||
let workflowJson;
|
||||
try {
|
||||
workflowJson = await fsReadFile(workflowPath, { encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`The file "${workflowPath}" could not be found, [item ${itemIndex}]`,
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
workflowInfo.code = jsonParse(workflowJson);
|
||||
} else if (source === 'parameter') {
|
||||
// Read workflow from parameter
|
||||
const workflowJson = this.getNodeParameter('workflowJson', itemIndex) as string;
|
||||
workflowInfo.code = jsonParse(workflowJson);
|
||||
} else if (source === 'url') {
|
||||
// Read workflow from url
|
||||
const workflowUrl = this.getNodeParameter('workflowUrl', itemIndex) as string;
|
||||
|
||||
const requestOptions = {
|
||||
headers: {
|
||||
accept: 'application/json,text/*;q=0.99',
|
||||
},
|
||||
method: 'GET',
|
||||
uri: workflowUrl,
|
||||
json: true,
|
||||
gzip: true,
|
||||
} satisfies IRequestOptions;
|
||||
|
||||
const response = await this.helpers.request(requestOptions);
|
||||
workflowInfo.code = response;
|
||||
}
|
||||
|
||||
return workflowInfo;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import {
|
||||
EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
|
||||
type ILocalLoadOptionsFunctions,
|
||||
type ResourceMapperField,
|
||||
type ResourceMapperFields,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { getFieldEntries } from '../../GenericFunctions';
|
||||
|
||||
export async function loadWorkflowInputMappings(
|
||||
this: ILocalLoadOptionsFunctions,
|
||||
): Promise<ResourceMapperFields> {
|
||||
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: true,
|
||||
display: true,
|
||||
canBeUsedToMatch: true,
|
||||
};
|
||||
|
||||
if (currentWorkflowInput.type !== 'any') {
|
||||
field.type = currentWorkflowInput.type;
|
||||
}
|
||||
|
||||
return field;
|
||||
});
|
||||
}
|
||||
return { fields };
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
import {
|
||||
NodeConnectionType,
|
||||
type IExecuteFunctions,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
INPUT_SOURCE,
|
||||
WORKFLOW_INPUTS,
|
||||
JSON_EXAMPLE,
|
||||
VALUES,
|
||||
INPUT_OPTIONS,
|
||||
TYPE_OPTIONS,
|
||||
} from '../constants';
|
||||
import { getFieldEntries, getWorkflowInputData } from '../GenericFunctions';
|
||||
|
||||
export class ExecuteWorkflowTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Execute Workflow Trigger',
|
||||
name: 'executeWorkflowTrigger',
|
||||
icon: 'fa:sign-out-alt',
|
||||
group: ['trigger'],
|
||||
version: [1, 1.1],
|
||||
description:
|
||||
'Helpers for calling other n8n workflows. Used for designing modular, microservice-like workflows.',
|
||||
eventTriggerDescription: '',
|
||||
maxNodes: 1,
|
||||
defaults: {
|
||||
name: 'Workflow Input Trigger',
|
||||
color: '#ff6d5a',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: [NodeConnectionType.Main],
|
||||
hints: [
|
||||
{
|
||||
message:
|
||||
'You need to define your input fields explicitly. Otherwise the parent cannot provide data and you will not receive input data.',
|
||||
// This condition checks if we have no input fields, which gets a bit awkward:
|
||||
// For WORKFLOW_INPUTS: keys() only contains `VALUES` if at least one value is provided
|
||||
// For JSON_EXAMPLE: We remove all whitespace and check if we're left with an empty object. Note that we already error if the example is not valid JSON
|
||||
displayCondition:
|
||||
`={{$parameter['${INPUT_SOURCE}'] === '${WORKFLOW_INPUTS}' && !$parameter['${WORKFLOW_INPUTS}'].keys().length ` +
|
||||
`|| $parameter['${INPUT_SOURCE}'] === '${JSON_EXAMPLE}' && $parameter['${JSON_EXAMPLE}'].toString().replaceAll(' ', '').replaceAll('\\n', '') === '{}' }}`,
|
||||
whenToDisplay: 'always',
|
||||
location: 'ndv',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Events',
|
||||
name: 'events',
|
||||
type: 'hidden',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Workflow Call',
|
||||
value: 'worklfow_call',
|
||||
description: 'When called by another workflow using Execute Workflow Trigger',
|
||||
action: 'When Called by Another Workflow',
|
||||
},
|
||||
],
|
||||
default: 'worklfow_call',
|
||||
},
|
||||
{
|
||||
displayName: 'Input Source',
|
||||
name: INPUT_SOURCE,
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Using Fields Below',
|
||||
value: WORKFLOW_INPUTS,
|
||||
description: 'Provide via UI',
|
||||
},
|
||||
{
|
||||
name: 'Using JSON Example',
|
||||
value: JSON_EXAMPLE,
|
||||
description: 'Infer JSON schema via JSON example output',
|
||||
},
|
||||
],
|
||||
default: WORKFLOW_INPUTS,
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { gte: 1.1 } }] },
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'Provide an example object to infer fields and their types.<br>To allow any type for a given field, set the value to null.',
|
||||
name: `${JSON_EXAMPLE}_notice`,
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [JSON_EXAMPLE] },
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'JSON Example',
|
||||
name: JSON_EXAMPLE,
|
||||
type: 'json',
|
||||
default: JSON.stringify(
|
||||
{
|
||||
aField: 'a string',
|
||||
aNumber: 123,
|
||||
thisFieldAcceptsAnyType: null,
|
||||
anArray: [],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [JSON_EXAMPLE] },
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Workflow Inputs',
|
||||
name: WORKFLOW_INPUTS,
|
||||
placeholder: 'Add Field',
|
||||
type: 'fixedCollection',
|
||||
description:
|
||||
'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
sortable: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [WORKFLOW_INPUTS] },
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: VALUES,
|
||||
displayName: 'Values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. fieldName',
|
||||
description: 'Name of the field',
|
||||
noDataExpression: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
description: 'The field value type',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||
options: TYPE_OPTIONS,
|
||||
default: 'string',
|
||||
noDataExpression: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Input Options',
|
||||
name: INPUT_OPTIONS,
|
||||
placeholder: 'Options',
|
||||
type: 'collection',
|
||||
description: 'Options controlling how input data is handled, converted and rejected',
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { gte: 1.1 } }] },
|
||||
},
|
||||
default: {},
|
||||
// Note that, while the defaults are true, the user has to add these in the first place
|
||||
// We default to false if absent in the execute function below
|
||||
options: [
|
||||
{
|
||||
displayName: 'Attempt to Convert Types',
|
||||
name: 'attemptToConvertTypes',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to attempt conversion on type mismatch, rather than directly returning an Error',
|
||||
noDataExpression: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Ignore Type Mismatch Errors',
|
||||
name: 'ignoreTypeErrors',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether type mismatches should be ignored, rather than returning an Error',
|
||||
noDataExpression: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
const inputData = this.getInputData();
|
||||
|
||||
if (this.getNode().typeVersion < 1.1) {
|
||||
return [inputData];
|
||||
} else {
|
||||
const newParams = getFieldEntries(this);
|
||||
|
||||
return [getWorkflowInputData.call(this, inputData, newParams)];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,67 +1,171 @@
|
|||
import { readFile as fsReadFile } from 'fs/promises';
|
||||
import { NodeOperationError, jsonParse } from 'n8n-workflow';
|
||||
import { json as generateSchemaFromExample, type SchemaObject } from 'generate-schema';
|
||||
import type { JSONSchema7 } from 'json-schema';
|
||||
import type {
|
||||
FieldValueOption,
|
||||
FieldType,
|
||||
IWorkflowNodeContext,
|
||||
INodeExecutionData,
|
||||
IExecuteFunctions,
|
||||
IExecuteWorkflowInfo,
|
||||
INodeParameterResourceLocator,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
import { jsonParse, NodeOperationError, validateFieldType } from 'n8n-workflow';
|
||||
|
||||
export async function getWorkflowInfo(this: IExecuteFunctions, source: string, itemIndex = 0) {
|
||||
const workflowInfo: IExecuteWorkflowInfo = {};
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
if (source === 'database') {
|
||||
// Read workflow from database
|
||||
if (nodeVersion === 1) {
|
||||
workflowInfo.id = this.getNodeParameter('workflowId', itemIndex) as string;
|
||||
} else {
|
||||
const { value } = this.getNodeParameter(
|
||||
'workflowId',
|
||||
itemIndex,
|
||||
{},
|
||||
) as INodeParameterResourceLocator;
|
||||
workflowInfo.id = value as string;
|
||||
}
|
||||
} else if (source === 'localFile') {
|
||||
// Read workflow from filesystem
|
||||
const workflowPath = this.getNodeParameter('workflowPath', itemIndex) as string;
|
||||
import {
|
||||
JSON_EXAMPLE,
|
||||
INPUT_SOURCE,
|
||||
WORKFLOW_INPUTS,
|
||||
VALUES,
|
||||
TYPE_OPTIONS,
|
||||
INPUT_OPTIONS,
|
||||
FALLBACK_DEFAULT_VALUE,
|
||||
} from './constants';
|
||||
|
||||
let workflowJson;
|
||||
try {
|
||||
workflowJson = await fsReadFile(workflowPath, { encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`The file "${workflowPath}" could not be found, [item ${itemIndex}]`,
|
||||
);
|
||||
}
|
||||
const SUPPORTED_TYPES = TYPE_OPTIONS.map((x) => x.value);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
workflowInfo.code = jsonParse(workflowJson);
|
||||
} else if (source === 'parameter') {
|
||||
// Read workflow from parameter
|
||||
const workflowJson = this.getNodeParameter('workflowJson', itemIndex) as string;
|
||||
workflowInfo.code = jsonParse(workflowJson);
|
||||
} else if (source === 'url') {
|
||||
// Read workflow from url
|
||||
const workflowUrl = this.getNodeParameter('workflowUrl', itemIndex) as string;
|
||||
|
||||
const requestOptions = {
|
||||
headers: {
|
||||
accept: 'application/json,text/*;q=0.99',
|
||||
},
|
||||
method: 'GET',
|
||||
uri: workflowUrl,
|
||||
json: true,
|
||||
gzip: true,
|
||||
} satisfies IRequestOptions;
|
||||
|
||||
const response = await this.helpers.request(requestOptions);
|
||||
workflowInfo.code = response;
|
||||
function parseJsonSchema(schema: JSONSchema7): FieldValueOption[] | string {
|
||||
if (!schema?.properties) {
|
||||
return 'Invalid JSON schema. Missing key `properties` in schema';
|
||||
}
|
||||
|
||||
return workflowInfo;
|
||||
if (typeof schema.properties !== 'object') {
|
||||
return 'Invalid JSON schema. Key `properties` is not an object';
|
||||
}
|
||||
|
||||
const result: FieldValueOption[] = [];
|
||||
for (const [name, v] of Object.entries(schema.properties)) {
|
||||
if (typeof v !== 'object') {
|
||||
return `Invalid JSON schema. Value for property '${name}' is not an object`;
|
||||
}
|
||||
|
||||
const type = v?.type;
|
||||
|
||||
if (type === 'null') {
|
||||
result.push({ name, type: 'any' });
|
||||
} else if (Array.isArray(type)) {
|
||||
// Schema allows an array of types, but we don't
|
||||
return `Invalid JSON schema. Array of types for property '${name}' is not supported by n8n. Either provide a single type or use type 'any' to allow any type`;
|
||||
} else if (typeof type !== 'string') {
|
||||
return `Invalid JSON schema. Unexpected non-string type ${type} for property '${name}'`;
|
||||
} else if (!SUPPORTED_TYPES.includes(type as never)) {
|
||||
return `Invalid JSON schema. Unsupported type ${type} for property '${name}'. Supported types are ${JSON.stringify(SUPPORTED_TYPES, null, 1)}`;
|
||||
} else {
|
||||
result.push({ name, type: type as FieldType });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseJsonExample(context: IWorkflowNodeContext): JSONSchema7 {
|
||||
const jsonString = context.getNodeParameter(JSON_EXAMPLE, 0, '') as string;
|
||||
const json = jsonParse<SchemaObject>(jsonString);
|
||||
|
||||
return generateSchemaFromExample(json) as JSONSchema7;
|
||||
}
|
||||
|
||||
export function getFieldEntries(context: IWorkflowNodeContext): FieldValueOption[] {
|
||||
const inputSource = context.getNodeParameter(INPUT_SOURCE, 0);
|
||||
let result: FieldValueOption[] | string = 'Internal Error: Invalid input source';
|
||||
try {
|
||||
if (inputSource === WORKFLOW_INPUTS) {
|
||||
result = context.getNodeParameter(
|
||||
`${WORKFLOW_INPUTS}.${VALUES}`,
|
||||
0,
|
||||
[],
|
||||
) as FieldValueOption[];
|
||||
} else if (inputSource === JSON_EXAMPLE) {
|
||||
const schema = parseJsonExample(context);
|
||||
result = parseJsonSchema(schema);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
result =
|
||||
e && typeof e === 'object' && 'message' in e && typeof e.message === 'string'
|
||||
? e.message
|
||||
: `Unknown error occurred: ${JSON.stringify(e)}`;
|
||||
}
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
return result;
|
||||
}
|
||||
throw new NodeOperationError(context.getNode(), result);
|
||||
}
|
||||
|
||||
export function getWorkflowInputData(
|
||||
this: IExecuteFunctions,
|
||||
inputData: INodeExecutionData[],
|
||||
newParams: FieldValueOption[],
|
||||
): INodeExecutionData[] {
|
||||
const items: INodeExecutionData[] = [];
|
||||
|
||||
for (const [itemIndex, item] of inputData.entries()) {
|
||||
const attemptToConvertTypes = this.getNodeParameter(
|
||||
`${INPUT_OPTIONS}.attemptToConvertTypes`,
|
||||
itemIndex,
|
||||
false,
|
||||
);
|
||||
const ignoreTypeErrors = this.getNodeParameter(
|
||||
`${INPUT_OPTIONS}.ignoreTypeErrors`,
|
||||
itemIndex,
|
||||
false,
|
||||
);
|
||||
|
||||
// Fields listed here will explicitly overwrite original fields
|
||||
const newItem: INodeExecutionData = {
|
||||
json: {},
|
||||
index: itemIndex,
|
||||
// TODO: Ensure we handle sub-execution jumps correctly.
|
||||
// metadata: {
|
||||
// subExecution: {
|
||||
// executionId: 'uhh',
|
||||
// workflowId: 'maybe?',
|
||||
// },
|
||||
// },
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
|
36
packages/nodes-base/nodes/ExecuteWorkflow/constants.ts
Normal file
36
packages/nodes-base/nodes/ExecuteWorkflow/constants.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
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 TYPE_OPTIONS: Array<{ name: string; value: FieldType | 'any' }> = [
|
||||
{
|
||||
name: 'Allow Any Type',
|
||||
value: 'any',
|
||||
},
|
||||
{
|
||||
name: 'String',
|
||||
value: 'string',
|
||||
},
|
||||
{
|
||||
name: 'Number',
|
||||
value: 'number',
|
||||
},
|
||||
{
|
||||
name: 'Boolean',
|
||||
value: 'boolean',
|
||||
},
|
||||
{
|
||||
name: 'Array',
|
||||
value: 'array',
|
||||
},
|
||||
{
|
||||
name: 'Object',
|
||||
value: 'object',
|
||||
},
|
||||
// Intentional omission of `dateTime`, `time`, `string-alphanumeric`, `form-fields`, `jwt` and `url`
|
||||
];
|
||||
|
||||
export const FALLBACK_DEFAULT_VALUE = null;
|
|
@ -1,381 +0,0 @@
|
|||
import { json as generateSchemaFromExample, type SchemaObject } from 'generate-schema';
|
||||
import type { JSONSchema7 } from 'json-schema';
|
||||
import {
|
||||
type INodeExecutionData,
|
||||
NodeConnectionType,
|
||||
NodeOperationError,
|
||||
type IExecuteFunctions,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
validateFieldType,
|
||||
type FieldType,
|
||||
jsonParse,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
const INPUT_SOURCE = 'inputSource';
|
||||
const WORKFLOW_INPUTS = 'workflowInputs';
|
||||
const INPUT_OPTIONS = 'inputOptions';
|
||||
const VALUES = 'values';
|
||||
const JSON_EXAMPLE = 'jsonExample';
|
||||
const TYPE_OPTIONS: Array<{ name: string; value: FieldType | 'any' }> = [
|
||||
{
|
||||
name: 'Allow Any Type',
|
||||
value: 'any',
|
||||
},
|
||||
{
|
||||
name: 'String',
|
||||
value: 'string',
|
||||
},
|
||||
{
|
||||
name: 'Number',
|
||||
value: 'number',
|
||||
},
|
||||
{
|
||||
name: 'Boolean',
|
||||
value: 'boolean',
|
||||
},
|
||||
{
|
||||
name: 'Array',
|
||||
value: 'array',
|
||||
},
|
||||
{
|
||||
name: 'Object',
|
||||
value: 'object',
|
||||
},
|
||||
// Intentional omission of `dateTime`, `time`, `string-alphanumeric`, `form-fields`, `jwt` and `url`
|
||||
];
|
||||
const SUPPORTED_TYPES = TYPE_OPTIONS.map((x) => x.value);
|
||||
|
||||
const DEFAULT_PLACEHOLDER = null;
|
||||
|
||||
type ValueOptions = { name: string; type: FieldType | 'any' };
|
||||
|
||||
function parseJsonSchema(schema: JSONSchema7): ValueOptions[] | string {
|
||||
if (!schema?.properties) {
|
||||
return 'Invalid JSON schema. Missing key `properties` in schema';
|
||||
}
|
||||
|
||||
if (typeof schema.properties !== 'object') {
|
||||
return 'Invalid JSON schema. Key `properties` is not an object';
|
||||
}
|
||||
|
||||
const result: ValueOptions[] = [];
|
||||
for (const [name, v] of Object.entries(schema.properties)) {
|
||||
if (typeof v !== 'object') {
|
||||
return `Invalid JSON schema. Value for property '${name}' is not an object`;
|
||||
}
|
||||
|
||||
const type = v?.type;
|
||||
|
||||
if (type === 'null') {
|
||||
result.push({ name, type: 'any' });
|
||||
} else if (Array.isArray(type)) {
|
||||
// Schema allows an array of types, but we don't
|
||||
return `Invalid JSON schema. Array of types for property '${name}' is not supported by n8n. Either provide a single type or use type 'any' to allow any type`;
|
||||
} else if (typeof type !== 'string') {
|
||||
return `Invalid JSON schema. Unexpected non-string type ${type} for property '${name}'`;
|
||||
} else if (!SUPPORTED_TYPES.includes(type as never)) {
|
||||
return `Invalid JSON schema. Unsupported type ${type} for property '${name}'. Supported types are ${JSON.stringify(SUPPORTED_TYPES, null, 1)}`;
|
||||
} else {
|
||||
result.push({ name, type: type as FieldType });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseJsonExample(context: IExecuteFunctions): JSONSchema7 {
|
||||
const jsonString = context.getNodeParameter(JSON_EXAMPLE, 0, '') as string;
|
||||
const json = jsonParse<SchemaObject>(jsonString);
|
||||
|
||||
return generateSchemaFromExample(json) as JSONSchema7;
|
||||
}
|
||||
|
||||
function getFieldEntries(context: IExecuteFunctions): ValueOptions[] {
|
||||
const inputSource = context.getNodeParameter(INPUT_SOURCE, 0) as string;
|
||||
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;
|
||||
}>;
|
||||
} else if (inputSource === JSON_EXAMPLE) {
|
||||
const schema = parseJsonExample(context);
|
||||
result = parseJsonSchema(schema);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
result =
|
||||
e && typeof e === 'object' && 'message' in e && typeof e.message === 'string'
|
||||
? e.message
|
||||
: `Unknown error occurred: ${JSON.stringify(e)}`;
|
||||
}
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
return result;
|
||||
}
|
||||
throw new NodeOperationError(context.getNode(), result);
|
||||
}
|
||||
|
||||
export class ExecuteWorkflowTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Execute Workflow Trigger',
|
||||
name: 'executeWorkflowTrigger',
|
||||
icon: 'fa:sign-out-alt',
|
||||
group: ['trigger'],
|
||||
version: [1, 1.1],
|
||||
description:
|
||||
'Helpers for calling other n8n workflows. Used for designing modular, microservice-like workflows.',
|
||||
eventTriggerDescription: '',
|
||||
maxNodes: 1,
|
||||
defaults: {
|
||||
name: 'Workflow Input Trigger',
|
||||
color: '#ff6d5a',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: [NodeConnectionType.Main],
|
||||
hints: [
|
||||
{
|
||||
message:
|
||||
'You need to define your input fields explicitly. Otherwise the parent cannot provide data and you will not receive input data.',
|
||||
// This condition checks if we have no input fields, which gets a bit awkward:
|
||||
// For WORKFLOW_INPUTS: keys() only contains `VALUES` if at least one value is provided
|
||||
// For JSON_EXAMPLE: We remove all whitespace and check if we're left with an empty object. Note that we already error if the example is not valid JSON
|
||||
displayCondition:
|
||||
`={{$parameter['${INPUT_SOURCE}'] === '${WORKFLOW_INPUTS}' && !$parameter['${WORKFLOW_INPUTS}'].keys().length ` +
|
||||
`|| $parameter['${INPUT_SOURCE}'] === '${JSON_EXAMPLE}' && $parameter['${JSON_EXAMPLE}'].toString().replaceAll(' ', '').replaceAll('\\n', '') === '{}' }}`,
|
||||
whenToDisplay: 'always',
|
||||
location: 'ndv',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Events',
|
||||
name: 'events',
|
||||
type: 'hidden',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Workflow Call',
|
||||
value: 'worklfow_call',
|
||||
description: 'When called by another workflow using Execute Workflow Trigger',
|
||||
action: 'When Called by Another Workflow',
|
||||
},
|
||||
],
|
||||
default: 'worklfow_call',
|
||||
},
|
||||
{
|
||||
displayName: 'Input Source',
|
||||
name: INPUT_SOURCE,
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Using Fields Below',
|
||||
value: WORKFLOW_INPUTS,
|
||||
description: 'Provide via UI',
|
||||
},
|
||||
{
|
||||
name: 'Using JSON Example',
|
||||
value: JSON_EXAMPLE,
|
||||
description: 'Infer JSON schema via JSON example output',
|
||||
},
|
||||
],
|
||||
default: WORKFLOW_INPUTS,
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { gte: 1.1 } }] },
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'Provide an example object to infer fields and their types.<br>To allow any type for a given field, set the value to null.',
|
||||
name: `${JSON_EXAMPLE}_notice`,
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [JSON_EXAMPLE] },
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'JSON Example',
|
||||
name: JSON_EXAMPLE,
|
||||
type: 'json',
|
||||
default: JSON.stringify(
|
||||
{
|
||||
aField: 'a string',
|
||||
aNumber: 123,
|
||||
thisFieldAcceptsAnyType: null,
|
||||
anArray: [],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [JSON_EXAMPLE] },
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Workflow Inputs',
|
||||
name: WORKFLOW_INPUTS,
|
||||
placeholder: 'Add Field',
|
||||
type: 'fixedCollection',
|
||||
description:
|
||||
'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
sortable: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [WORKFLOW_INPUTS] },
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: VALUES,
|
||||
displayName: 'Values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. fieldName',
|
||||
description: 'Name of the field',
|
||||
noDataExpression: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
description: 'The field value type',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||
options: TYPE_OPTIONS,
|
||||
default: 'string',
|
||||
noDataExpression: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Input Options',
|
||||
name: INPUT_OPTIONS,
|
||||
placeholder: 'Options',
|
||||
type: 'collection',
|
||||
description: 'Options controlling how input data is handled, converted and rejected',
|
||||
displayOptions: {
|
||||
show: { '@version': [{ _cnd: { gte: 1.1 } }] },
|
||||
},
|
||||
default: {},
|
||||
// Note that, while the defaults are true, the user has to add these in the first place
|
||||
// We default to false if absent in the execute function below
|
||||
options: [
|
||||
{
|
||||
displayName: 'Attempt to Convert Types',
|
||||
name: 'attemptToConvertTypes',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to attempt conversion on type mismatch, rather than directly returning an Error',
|
||||
noDataExpression: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Ignore Type Mismatch Errors',
|
||||
name: 'ignoreTypeErrors',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether type mismatches should be ignored, rather than returning an Error',
|
||||
noDataExpression: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
const inputData = this.getInputData();
|
||||
|
||||
if (this.getNode().typeVersion < 1.1) {
|
||||
return [inputData];
|
||||
} else {
|
||||
const items: INodeExecutionData[] = [];
|
||||
|
||||
for (const [itemIndex, item] of inputData.entries()) {
|
||||
const attemptToConvertTypes = this.getNodeParameter(
|
||||
`${INPUT_OPTIONS}.attemptToConvertTypes`,
|
||||
itemIndex,
|
||||
false,
|
||||
);
|
||||
const ignoreTypeErrors = this.getNodeParameter(
|
||||
`${INPUT_OPTIONS}.ignoreTypeErrors`,
|
||||
itemIndex,
|
||||
false,
|
||||
);
|
||||
|
||||
// Fields listed here will explicitly overwrite original fields
|
||||
const newItem: INodeExecutionData = {
|
||||
json: {},
|
||||
index: itemIndex,
|
||||
// TODO: Ensure we handle sub-execution jumps correctly.
|
||||
// metadata: {
|
||||
// subExecution: {
|
||||
// executionId: 'uhh',
|
||||
// workflowId: 'maybe?',
|
||||
// },
|
||||
// },
|
||||
pairedItem: { item: itemIndex },
|
||||
};
|
||||
try {
|
||||
const newParams = getFieldEntries(this);
|
||||
|
||||
for (const { name, type } of newParams) {
|
||||
if (!item.json.hasOwnProperty(name)) {
|
||||
newItem.json[name] = DEFAULT_PLACEHOLDER;
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [items];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -492,8 +492,8 @@
|
|||
"dist/nodes/ErrorTrigger/ErrorTrigger.node.js",
|
||||
"dist/nodes/Eventbrite/EventbriteTrigger.node.js",
|
||||
"dist/nodes/ExecuteCommand/ExecuteCommand.node.js",
|
||||
"dist/nodes/ExecuteWorkflow/ExecuteWorkflow.node.js",
|
||||
"dist/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.js",
|
||||
"dist/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.js",
|
||||
"dist/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.js",
|
||||
"dist/nodes/ExecutionData/ExecutionData.node.js",
|
||||
"dist/nodes/Facebook/FacebookGraphApi.node.js",
|
||||
"dist/nodes/Facebook/FacebookTrigger.node.js",
|
||||
|
|
|
@ -1068,9 +1068,23 @@ export interface ILoadOptionsFunctions extends FunctionsBase {
|
|||
options?: IGetNodeParameterOptions,
|
||||
): NodeParameterValueType | object | undefined;
|
||||
getCurrentNodeParameters(): INodeParameters | undefined;
|
||||
|
||||
helpers: RequestHelperFunctions & SSHTunnelFunctions;
|
||||
}
|
||||
|
||||
export type FieldValueOption = { name: string; type: FieldType | 'any' };
|
||||
|
||||
export type IWorkflowNodeContext = ExecuteFunctions.GetNodeParameterFn &
|
||||
Pick<FunctionsBase, 'getNode'>;
|
||||
|
||||
export interface ILocalLoadOptionsFunctions {
|
||||
getWorkflowNodeContext(nodeType: string): Promise<IWorkflowNodeContext | null>;
|
||||
}
|
||||
|
||||
export interface IWorkflowLoader {
|
||||
get(workflowId: string): Promise<IWorkflowBase>;
|
||||
}
|
||||
|
||||
export interface IPollFunctions
|
||||
extends FunctionsBaseWithRequiredKeys<'getMode' | 'getActivationMode'> {
|
||||
__emit(
|
||||
|
@ -1351,11 +1365,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;
|
||||
|
@ -1367,6 +1383,20 @@ export interface ResourceMapperTypeOptions {
|
|||
};
|
||||
}
|
||||
|
||||
// Enforce at least one of resourceMapperMethod or localResourceMapperMethod
|
||||
export type ResourceMapperTypeOptionsLocal = {
|
||||
resourceMapperMethod: string;
|
||||
localResourceMapperMethod?: never; // Explicitly disallows this property
|
||||
};
|
||||
|
||||
export type ResourceMapperTypeOptionsExternal = {
|
||||
localResourceMapperMethod: string;
|
||||
resourceMapperMethod?: never; // Explicitly disallows this property
|
||||
};
|
||||
|
||||
export type ResourceMapperTypeOptions = ResourceMapperTypeOptionsBase &
|
||||
(ResourceMapperTypeOptionsLocal | ResourceMapperTypeOptionsExternal);
|
||||
|
||||
type NonEmptyArray<T> = [T, ...T[]];
|
||||
|
||||
export type FilterTypeCombinator = 'and' | 'or';
|
||||
|
@ -1637,6 +1667,9 @@ export interface INodeType {
|
|||
resourceMapping?: {
|
||||
[functionName: string]: (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
||||
};
|
||||
localResourceMapping?: {
|
||||
[functionName: string]: (this: ILocalLoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
||||
};
|
||||
actionHandler?: {
|
||||
[functionName: string]: (
|
||||
this: ILoadOptionsFunctions,
|
||||
|
|
Loading…
Reference in a new issue