diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index befa2c6680..d5b471def9 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -20,6 +20,7 @@ import { import { IDataObject, IExecuteData, + IExecuteWorkflowInfo, INode, INodeParameters, INodeExecutionData, @@ -270,23 +271,32 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { * @param {INodeExecutionData[]} [inputData] * @returns {(Promise>)} */ -export async function executeWorkflow(workflowId: string, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[]): Promise> { +export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[]): Promise> { const mode = 'integrated'; + if (workflowInfo.id === undefined && workflowInfo.code === undefined) { + throw new Error(`No information about the workflow to execute found. Please provide either the "id" or "code"!`); + } + if (Db.collections!.Workflow === null) { // The first time executeWorkflow gets called the Database has // to get initialized first await Db.init(); } - const workflowData = await Db.collections!.Workflow!.findOne(workflowId); - if (workflowData === undefined) { - throw new Error(`The workflow with the id "${workflowId}" does not exist.`); + let workflowData: IWorkflowBase | undefined; + if (workflowInfo.id !== undefined) { + workflowData = await Db.collections!.Workflow!.findOne(workflowInfo.id); + if (workflowData === undefined) { + throw new Error(`The workflow with the id "${workflowInfo.id}" does not exist.`); + } + } else { + workflowData = workflowInfo.code; } const nodeTypes = NodeTypes(); - const workflow = new Workflow(workflowId as string | undefined, workflowData!.nodes, workflowData!.connections, workflowData!.active, nodeTypes, workflowData!.staticData); + const workflow = new Workflow(workflowInfo.id, workflowData!.nodes, workflowData!.connections, workflowData!.active, nodeTypes, workflowData!.staticData); // Does not get used so set it simply to empty string const executionId = ''; @@ -294,7 +304,7 @@ export async function executeWorkflow(workflowId: string, additionalData: IWorkf // Create new additionalData to have different workflow loaded and to call // different webooks const additionalDataIntegrated = await getBase(additionalData.credentials); - additionalDataIntegrated.hooks = getWorkflowHooksIntegrated(mode, executionId, workflowData, { parentProcessMode: additionalData.hooks!.mode }); + additionalDataIntegrated.hooks = getWorkflowHooksIntegrated(mode, executionId, workflowData!, { parentProcessMode: additionalData.hooks!.mode }); // Find Start-Node const requiredNodeTypes = ['n8n-nodes-base.start']; diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 4fc7c2dd40..2f90d0118f 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -13,6 +13,7 @@ import { IDataObject, IExecuteFunctions, IExecuteSingleFunctions, + IExecuteWorkflowInfo, INode, INodeExecutionData, INodeParameters, @@ -430,9 +431,8 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunExecutionData, runIndex: number, connectionInputData: INodeExecutionData[], inputData: ITaskDataConnections, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IExecuteFunctions { return ((workflow, runExecutionData, connectionInputData, inputData, node) => { return { - async executeWorkflow(workflowId: string, inputData?: INodeExecutionData[]): Promise { // tslint:disable-line:no-any - // return additionalData.executeWorkflow(workflowId, additionalData, inputData); - return additionalData.executeWorkflow(workflowId, additionalData, inputData); + async executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise { // tslint:disable-line:no-any + return additionalData.executeWorkflow(workflowInfo, additionalData, inputData); }, getContext(type: string): IContextObject { return NodeHelpers.getContext(runExecutionData, type, node); diff --git a/packages/nodes-base/nodes/ExecuteWorkflow.node.ts b/packages/nodes-base/nodes/ExecuteWorkflow.node.ts index bf5090e4c4..1ce92965fd 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow.node.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow.node.ts @@ -1,5 +1,12 @@ import { OptionsWithUri } from 'request'; +import { + readFile as fsReadFile, +} from 'fs'; +import { promisify } from 'util'; + +const fsReadFileAsync = promisify(fsReadFile); + import { IExecuteFunctions } from 'n8n-core'; import { ILoadOptionsFunctions, @@ -7,6 +14,8 @@ import { INodePropertyOptions, INodeType, INodeTypeDescription, + IExecuteWorkflowInfo, + IWorkflowBase, } from 'n8n-workflow'; @@ -26,10 +35,50 @@ export class ExecuteWorkflow implements INodeType { inputs: ['main'], outputs: ['main'], properties: [ + { + displayName: 'Source', + name: 'source', + type: 'options', + options: [ + { + name: 'Database', + value: 'database', + description: 'Load the workflow from the database by ID.', + }, + { + name: 'Local File', + value: 'localFile', + description: 'Load the workflow from a locally saved file.', + }, + { + name: 'Parameter', + value: 'parameter', + description: 'Load the workflow from a parameter.', + }, + { + name: 'URL', + value: 'url', + description: 'Load the workflow from an URL.', + }, + ], + default: 'database', + description: 'Where to get the workflow to execute from.', + }, + + // ---------------------------------- + // source:database + // ---------------------------------- { displayName: 'Workflow', name: 'workflowId', type: 'options', + displayOptions: { + show: { + source: [ + 'database', + ], + }, + }, typeOptions: { loadOptionsMethod: 'getWorkflows', }, @@ -37,6 +86,70 @@ export class ExecuteWorkflow implements INodeType { required: true, description: 'The workflow to execute.', }, + + // ---------------------------------- + // source:localFile + // ---------------------------------- + { + displayName: 'Workflow Path', + name: 'workflowPath', + type: 'string', + displayOptions: { + show: { + source: [ + 'localFile', + ], + }, + }, + default: '', + placeholder: '/data/workflow.json', + required: true, + description: 'The path to local JSON workflow file to execute.', + }, + + // ---------------------------------- + // source:parameter + // ---------------------------------- + { + displayName: 'Workflow JSON', + name: 'workflowJson', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + editor: 'code', + rows: 10, + }, + displayOptions: { + show: { + source: [ + 'parameter', + ], + }, + }, + default: '\n\n\n', + required: true, + description: 'The workflow JSON code to execute.', + }, + + // ---------------------------------- + // source:url + // ---------------------------------- + { + displayName: 'Workflow URL', + name: 'workflowUrl', + type: 'string', + displayOptions: { + show: { + source: [ + 'url', + ], + }, + }, + default: '', + placeholder: 'https://example.com/workflow.json', + required: true, + description: 'The URL from which to load the workflow from.', + }, ] }; @@ -67,8 +180,55 @@ export class ExecuteWorkflow implements INodeType { async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); - const workflowId = this.getNodeParameter('workflowId', 0) as string; - const receivedData = await this.executeWorkflow(workflowId, items); + const source = this.getNodeParameter('source', 0) as string; + + const workflowInfo: IExecuteWorkflowInfo = {}; + if (source === 'database') { + // Read workflow from database + workflowInfo.id = this.getNodeParameter('workflowId', 0) as string; + + } else if (source === 'localFile') { + // Read workflow from filesystem + const workflowPath = this.getNodeParameter('workflowPath', 0) as string; + + let workflowJson; + try { + workflowJson = await fsReadFileAsync(workflowPath, { encoding: 'utf8' }) as string; + } catch (error) { + if (error.code === 'ENOENT') { + throw new Error(`The file "${workflowPath}" could not be found.`); + } + + throw error; + } + + workflowInfo.code = JSON.parse(workflowJson) as IWorkflowBase; + } else if (source === 'parameter') { + // Read workflow from parameter + const workflowJson = this.getNodeParameter('workflowJson', 0) as string; + workflowInfo.code = JSON.parse(workflowJson) as IWorkflowBase; + + } else if (source === 'url') { + // Read workflow from url + const workflowUrl = this.getNodeParameter('workflowUrl', 0) as string; + + + const requestOptions = { + headers: { + 'accept': 'application/json,text/*;q=0.99', + }, + method: 'GET', + uri: workflowUrl, + json: true, + gzip: true, + }; + + const response = await this.helpers.request(requestOptions); + workflowInfo.code = response; + + } + + const receivedData = await this.executeWorkflow(workflowInfo, items); return receivedData; } diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index be0330d810..2aa49c1acb 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -154,7 +154,7 @@ export interface IExecuteContextData { export interface IExecuteFunctions { - executeWorkflow(workflowId: string, inputData?: INodeExecutionData[]): Promise; // tslint:disable-line:no-any + executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise; // tslint:disable-line:no-any getContext(type: string): IContextObject; getCredentials(type: string): ICredentialDataDecryptedObject | undefined; getInputData(inputIndex?: number, inputName?: string): INodeExecutionData[]; @@ -186,6 +186,11 @@ export interface IExecuteSingleFunctions { }; } +export interface IExecuteWorkflowInfo { + code?: IWorkflowBase; + id?: string; +} + export interface ILoadOptionsFunctions { getCredentials(type: string): ICredentialDataDecryptedObject | undefined; getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any @@ -636,7 +641,7 @@ export interface IWorkflowExecuteHooks { export interface IWorkflowExecuteAdditionalData { credentials: IWorkflowCredentials; encryptionKey: string; - executeWorkflow: (workflowId: string, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[]) => Promise; // tslint:disable-line:no-any + executeWorkflow: (workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[]) => Promise; // tslint:disable-line:no-any // hooks?: IWorkflowExecuteHooks; hooks?: WorkflowHooks; httpResponse?: express.Response;