mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -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')
|
@Post('/action-result')
|
||||||
async getActionResult(
|
async getActionResult(
|
||||||
req: DynamicNodeParametersRequest.ActionResult,
|
req: DynamicNodeParametersRequest.ActionResult,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { LoadOptionsContext, NodeExecuteFunctions } from 'n8n-core';
|
import { LoadOptionsContext, NodeExecuteFunctions, LocalLoadOptionsContext } from 'n8n-core';
|
||||||
import type {
|
import type {
|
||||||
ILoadOptions,
|
ILoadOptions,
|
||||||
ILoadOptionsFunctions,
|
ILoadOptionsFunctions,
|
||||||
|
@ -17,15 +17,43 @@ import type {
|
||||||
INodeTypeNameVersion,
|
INodeTypeNameVersion,
|
||||||
NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
|
ILocalLoadOptionsFunctions,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { Workflow, RoutingNode, ApplicationError } from 'n8n-workflow';
|
import { Workflow, RoutingNode, ApplicationError } from 'n8n-workflow';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
import { NodeTypes } from '@/node-types';
|
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()
|
@Service()
|
||||||
export class DynamicNodeParametersService {
|
export class DynamicNodeParametersService {
|
||||||
constructor(private nodeTypes: NodeTypes) {}
|
constructor(
|
||||||
|
private nodeTypes: NodeTypes,
|
||||||
|
private workflowLoaderService: WorkflowLoaderService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** Returns the available options via a predefined method */
|
/** Returns the available options via a predefined method */
|
||||||
async getOptionsViaMethodName(
|
async getOptionsViaMethodName(
|
||||||
|
@ -159,6 +187,20 @@ export class DynamicNodeParametersService {
|
||||||
return method.call(thisArgs);
|
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 */
|
/** Returns the result of the action handler */
|
||||||
async getActionResult(
|
async getActionResult(
|
||||||
handler: string,
|
handler: string,
|
||||||
|
@ -181,33 +223,34 @@ export class DynamicNodeParametersService {
|
||||||
type: 'resourceMapping',
|
type: 'resourceMapping',
|
||||||
methodName: string,
|
methodName: string,
|
||||||
nodeType: INodeType,
|
nodeType: INodeType,
|
||||||
): (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
): ResourceMappingMethod;
|
||||||
private getMethod(
|
private getMethod(
|
||||||
type: 'listSearch',
|
type: 'localResourceMapping',
|
||||||
methodName: string,
|
methodName: string,
|
||||||
nodeType: INodeType,
|
nodeType: INodeType,
|
||||||
): (
|
): LocalResourceMappingMethod;
|
||||||
this: ILoadOptionsFunctions,
|
private getMethod(type: 'listSearch', methodName: string, nodeType: INodeType): ListSearchMethod;
|
||||||
filter?: string | undefined,
|
|
||||||
paginationToken?: string | undefined,
|
|
||||||
) => Promise<INodeListSearchResult>;
|
|
||||||
private getMethod(
|
private getMethod(
|
||||||
type: 'loadOptions',
|
type: 'loadOptions',
|
||||||
methodName: string,
|
methodName: string,
|
||||||
nodeType: INodeType,
|
nodeType: INodeType,
|
||||||
): (this: ILoadOptionsFunctions) => Promise<INodePropertyOptions[]>;
|
): LoadOptionsMethod;
|
||||||
private getMethod(
|
private getMethod(
|
||||||
type: 'actionHandler',
|
type: 'actionHandler',
|
||||||
methodName: string,
|
methodName: string,
|
||||||
nodeType: INodeType,
|
nodeType: INodeType,
|
||||||
): (this: ILoadOptionsFunctions, payload?: string) => Promise<NodeParameterValueType>;
|
): ActionHandlerMethod;
|
||||||
|
|
||||||
private getMethod(
|
private getMethod(
|
||||||
type: 'resourceMapping' | 'listSearch' | 'loadOptions' | 'actionHandler',
|
type:
|
||||||
|
| 'resourceMapping'
|
||||||
|
| 'localResourceMapping'
|
||||||
|
| 'listSearch'
|
||||||
|
| 'loadOptions'
|
||||||
|
| 'actionHandler',
|
||||||
methodName: string,
|
methodName: string,
|
||||||
nodeType: INodeType,
|
nodeType: INodeType,
|
||||||
) {
|
): NodeMethod {
|
||||||
const method = nodeType.methods?.[type]?.[methodName];
|
const method = nodeType.methods?.[type]?.[methodName] as NodeMethod;
|
||||||
if (typeof method !== 'function') {
|
if (typeof method !== 'function') {
|
||||||
throw new ApplicationError('Node type does not have method defined', {
|
throw new ApplicationError('Node type does not have method defined', {
|
||||||
tags: { nodeType: nodeType.description.name },
|
tags: { nodeType: nodeType.description.name },
|
||||||
|
@ -255,4 +298,16 @@ export class DynamicNodeParametersService {
|
||||||
const node = workflow.nodes['Temp-Node'];
|
const node = workflow.nodes['Temp-Node'];
|
||||||
return new LoadOptionsContext(workflow, node, additionalData, path);
|
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 { ExecuteSingleContext } from './execute-single-context';
|
||||||
export { HookContext } from './hook-context';
|
export { HookContext } from './hook-context';
|
||||||
export { LoadOptionsContext } from './load-options-context';
|
export { LoadOptionsContext } from './load-options-context';
|
||||||
|
export { LocalLoadOptionsContext } from './local-load-options-context';
|
||||||
export { PollContext } from './poll-context';
|
export { PollContext } from './poll-context';
|
||||||
export { SupplyDataContext } from './supply-data-context';
|
export { SupplyDataContext } from './supply-data-context';
|
||||||
export { TriggerContext } from './trigger-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(
|
export async function getNodeParameterActionResult(
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
sendData: DynamicNodeParameters.ActionResultRequest,
|
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) {
|
if (!props.node) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const methodName = props.parameter.typeOptions?.resourceMapper?.resourceMapperMethod;
|
|
||||||
if (typeof methodName !== 'string') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestParams: DynamicNodeParameters.ResourceMapperFieldsRequest = {
|
const requestParams: DynamicNodeParameters.ResourceMapperFieldsRequest = {
|
||||||
nodeTypeAndVersion: {
|
nodeTypeAndVersion: {
|
||||||
name: props.node?.type,
|
name: props.node?.type,
|
||||||
version: props.node.typeVersion,
|
version: props.node?.typeVersion,
|
||||||
},
|
},
|
||||||
currentNodeParameters: resolveRequiredParameters(
|
currentNodeParameters: resolveRequiredParameters(
|
||||||
props.parameter,
|
props.parameter,
|
||||||
|
@ -262,7 +256,33 @@ async function loadFieldsToMap(): Promise<void> {
|
||||||
methodName,
|
methodName,
|
||||||
credentials: props.node.credentials,
|
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) {
|
if (fetchedFields !== null) {
|
||||||
const newSchema = fetchedFields.fields.map((field) => {
|
const newSchema = fetchedFields.fields.map((field) => {
|
||||||
const existingField = state.paramValue.schema.find((f) => f.id === field.id);
|
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 (
|
const getNodeParameterActionResult = async (
|
||||||
sendData: DynamicNodeParameters.ActionResultRequest,
|
sendData: DynamicNodeParameters.ActionResultRequest,
|
||||||
) => {
|
) => {
|
||||||
|
@ -326,6 +336,7 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
||||||
visibleNodeTypesByInputConnectionTypeNames,
|
visibleNodeTypesByInputConnectionTypeNames,
|
||||||
isConfigurableNode,
|
isConfigurableNode,
|
||||||
getResourceMapperFields,
|
getResourceMapperFields,
|
||||||
|
getLocalResourceMapperFields,
|
||||||
getNodeParameterActionResult,
|
getNodeParameterActionResult,
|
||||||
getResourceLocatorResults,
|
getResourceLocatorResults,
|
||||||
getNodeParameterOptions,
|
getNodeParameterOptions,
|
||||||
|
|
|
@ -1,14 +1,34 @@
|
||||||
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
||||||
import type {
|
import type {
|
||||||
ExecuteWorkflowData,
|
ExecuteWorkflowData,
|
||||||
|
FieldValueOption,
|
||||||
IExecuteFunctions,
|
IExecuteFunctions,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
INodeType,
|
INodeType,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
|
ResourceMapperField,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { getWorkflowInfo } from './GenericFunctions';
|
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 {
|
export class ExecuteWorkflow implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
|
@ -187,6 +207,41 @@ export class ExecuteWorkflow implements INodeType {
|
||||||
default: '',
|
default: '',
|
||||||
displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } },
|
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',
|
displayName: 'Mode',
|
||||||
name: 'mode',
|
name: 'mode',
|
||||||
|
@ -228,10 +283,16 @@ export class ExecuteWorkflow implements INodeType {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
methods = {
|
||||||
|
localResourceMapping: {
|
||||||
|
loadWorkflowInputMappings,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
const source = this.getNodeParameter('source', 0) as string;
|
const source = this.getNodeParameter('source', 0) as string;
|
||||||
const mode = this.getNodeParameter('mode', 0, false) 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 workflowProxy = this.getWorkflowDataProxy(0);
|
||||||
const currentWorkflowId = workflowProxy.$workflow.id as string;
|
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 { json as generateSchemaFromExample, type SchemaObject } from 'generate-schema';
|
||||||
import { NodeOperationError, jsonParse } from 'n8n-workflow';
|
import type { JSONSchema7 } from 'json-schema';
|
||||||
import type {
|
import type {
|
||||||
|
FieldValueOption,
|
||||||
|
FieldType,
|
||||||
|
IWorkflowNodeContext,
|
||||||
|
INodeExecutionData,
|
||||||
IExecuteFunctions,
|
IExecuteFunctions,
|
||||||
IExecuteWorkflowInfo,
|
|
||||||
INodeParameterResourceLocator,
|
|
||||||
IRequestOptions,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import { jsonParse, NodeOperationError, validateFieldType } from 'n8n-workflow';
|
||||||
|
|
||||||
export async function getWorkflowInfo(this: IExecuteFunctions, source: string, itemIndex = 0) {
|
import {
|
||||||
const workflowInfo: IExecuteWorkflowInfo = {};
|
JSON_EXAMPLE,
|
||||||
const nodeVersion = this.getNode().typeVersion;
|
INPUT_SOURCE,
|
||||||
if (source === 'database') {
|
WORKFLOW_INPUTS,
|
||||||
// Read workflow from database
|
VALUES,
|
||||||
if (nodeVersion === 1) {
|
TYPE_OPTIONS,
|
||||||
workflowInfo.id = this.getNodeParameter('workflowId', itemIndex) as string;
|
INPUT_OPTIONS,
|
||||||
} else {
|
FALLBACK_DEFAULT_VALUE,
|
||||||
const { value } = this.getNodeParameter(
|
} from './constants';
|
||||||
'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;
|
const SUPPORTED_TYPES = TYPE_OPTIONS.map((x) => x.value);
|
||||||
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;
|
function parseJsonSchema(schema: JSONSchema7): FieldValueOption[] | string {
|
||||||
}
|
if (!schema?.properties) {
|
||||||
|
return 'Invalid JSON schema. Missing key `properties` in schema';
|
||||||
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;
|
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/ErrorTrigger/ErrorTrigger.node.js",
|
||||||
"dist/nodes/Eventbrite/EventbriteTrigger.node.js",
|
"dist/nodes/Eventbrite/EventbriteTrigger.node.js",
|
||||||
"dist/nodes/ExecuteCommand/ExecuteCommand.node.js",
|
"dist/nodes/ExecuteCommand/ExecuteCommand.node.js",
|
||||||
"dist/nodes/ExecuteWorkflow/ExecuteWorkflow.node.js",
|
"dist/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.js",
|
||||||
"dist/nodes/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.js",
|
"dist/nodes/ExecuteWorkflow/ExecuteWorkflowTrigger/ExecuteWorkflowTrigger.node.js",
|
||||||
"dist/nodes/ExecutionData/ExecutionData.node.js",
|
"dist/nodes/ExecutionData/ExecutionData.node.js",
|
||||||
"dist/nodes/Facebook/FacebookGraphApi.node.js",
|
"dist/nodes/Facebook/FacebookGraphApi.node.js",
|
||||||
"dist/nodes/Facebook/FacebookTrigger.node.js",
|
"dist/nodes/Facebook/FacebookTrigger.node.js",
|
||||||
|
|
|
@ -1068,9 +1068,23 @@ export interface ILoadOptionsFunctions extends FunctionsBase {
|
||||||
options?: IGetNodeParameterOptions,
|
options?: IGetNodeParameterOptions,
|
||||||
): NodeParameterValueType | object | undefined;
|
): NodeParameterValueType | object | undefined;
|
||||||
getCurrentNodeParameters(): INodeParameters | undefined;
|
getCurrentNodeParameters(): INodeParameters | undefined;
|
||||||
|
|
||||||
helpers: RequestHelperFunctions & SSHTunnelFunctions;
|
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
|
export interface IPollFunctions
|
||||||
extends FunctionsBaseWithRequiredKeys<'getMode' | 'getActivationMode'> {
|
extends FunctionsBaseWithRequiredKeys<'getMode' | 'getActivationMode'> {
|
||||||
__emit(
|
__emit(
|
||||||
|
@ -1351,11 +1365,13 @@ export interface INodePropertyTypeOptions {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResourceMapperTypeOptions {
|
export interface ResourceMapperTypeOptionsBase {
|
||||||
resourceMapperMethod: string;
|
|
||||||
mode: 'add' | 'update' | 'upsert';
|
mode: 'add' | 'update' | 'upsert';
|
||||||
valuesLabel?: string;
|
valuesLabel?: string;
|
||||||
fieldWords?: { singular: string; plural: string };
|
fieldWords?: {
|
||||||
|
singular: string;
|
||||||
|
plural: string;
|
||||||
|
};
|
||||||
addAllFields?: boolean;
|
addAllFields?: boolean;
|
||||||
noFieldsError?: string;
|
noFieldsError?: string;
|
||||||
multiKeyMatch?: boolean;
|
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[]];
|
type NonEmptyArray<T> = [T, ...T[]];
|
||||||
|
|
||||||
export type FilterTypeCombinator = 'and' | 'or';
|
export type FilterTypeCombinator = 'and' | 'or';
|
||||||
|
@ -1637,6 +1667,9 @@ export interface INodeType {
|
||||||
resourceMapping?: {
|
resourceMapping?: {
|
||||||
[functionName: string]: (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
[functionName: string]: (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
||||||
};
|
};
|
||||||
|
localResourceMapping?: {
|
||||||
|
[functionName: string]: (this: ILocalLoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
||||||
|
};
|
||||||
actionHandler?: {
|
actionHandler?: {
|
||||||
[functionName: string]: (
|
[functionName: string]: (
|
||||||
this: ILoadOptionsFunctions,
|
this: ILoadOptionsFunctions,
|
||||||
|
|
Loading…
Reference in a new issue