From db1bbf0432dc1e158834ec3fad05353198c8de9c Mon Sep 17 00:00:00 2001 From: Omar Ajoue Date: Mon, 15 Feb 2021 15:53:02 +0100 Subject: [PATCH] Added flag to simplify output to execute command and created executeAll Also created a command that lists workflows so it can be used by other applications that wish to interact with n8n via CLI. --- packages/cli/commands/execute.ts | 10 +- packages/cli/commands/executeAll.ts | 174 +++++++++++++++++++++++++ packages/cli/commands/list/workflow.ts | 66 ++++++++++ 3 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 packages/cli/commands/executeAll.ts create mode 100644 packages/cli/commands/list/workflow.ts diff --git a/packages/cli/commands/execute.ts b/packages/cli/commands/execute.ts index b3450e9ef2..502312e400 100644 --- a/packages/cli/commands/execute.ts +++ b/packages/cli/commands/execute.ts @@ -40,6 +40,9 @@ export class Execute extends Command { id: flags.string({ description: 'id of the workflow to execute', }), + rawOutput: flags.boolean({ + description: 'Outputs only JSON data, with no other text', + }), }; @@ -172,9 +175,10 @@ export class Execute extends Command { error.stack = data.data.resultData.error.stack; throw error; } - - this.log('Execution was successfull:'); - this.log('===================================='); + if (flags.rawOutput === undefined) { + this.log('Execution was successfull:'); + this.log('===================================='); + } this.log(JSON.stringify(data, null, 2)); } catch (e) { console.error('\nGOT ERROR'); diff --git a/packages/cli/commands/executeAll.ts b/packages/cli/commands/executeAll.ts new file mode 100644 index 0000000000..334d8e955f --- /dev/null +++ b/packages/cli/commands/executeAll.ts @@ -0,0 +1,174 @@ +import * as fs from 'fs'; +import { Command, flags } from '@oclif/command'; +import { + UserSettings, +} from 'n8n-core'; +import { + INode, +} from 'n8n-workflow'; + +import { + ActiveExecutions, + CredentialsOverwrites, + CredentialTypes, + Db, + ExternalHooks, + GenericHelpers, + IWorkflowBase, + IWorkflowExecutionDataProcess, + LoadNodesAndCredentials, + NodeTypes, + WorkflowCredentials, + WorkflowHelpers, + WorkflowRunner, +} from "../src"; + +import { + sep +} from 'path'; + + +export class ExecuteAll extends Command { + static description = '\nExecutes all workflows once'; + + static examples = [ + `$ n8n executeAll`, + `$ n8n executeAll --debug`, + `$ n8n executeAll --snapshot=/data/snapshots`, + ]; + + static flags = { + help: flags.help({ char: 'h' }), + debug: flags.boolean({ + description: 'Toggles on displaying all errors and debug messages.', + }), + snapshot: flags.string({ + description: 'Enables snapshot saving. You must inform an existing folder to save snapshots via this param.', + }), + }; + + + async run() { + const { flags } = this.parse(ExecuteAll); + + const debug = flags.debug !== undefined; + + if (flags.snapshot !== undefined) { + if (fs.existsSync(flags.snapshot)) { + if (!fs.lstatSync(flags.snapshot).isDirectory()) { + GenericHelpers.logOutput(`The paramenter --snapshot must be an existing directory`); + return; + } + } else { + GenericHelpers.logOutput(`The paramenter --snapshot must be an existing directory`); + return; + } + } + + // Start directly with the init of the database to improve startup time + const startDbInitPromise = Db.init(); + + // Load all node and credential types + const loadNodesAndCredentials = LoadNodesAndCredentials(); + const loadNodesAndCredentialsPromise = loadNodesAndCredentials.init(); + + // Wait till the database is ready + await startDbInitPromise; + + const allWorkflows = await Db.collections!.Workflow!.find(); + if (debug) { + this.log(`Found ${allWorkflows.length} workflows to execute.`); + } + + // Make sure the settings exist + await UserSettings.prepareUserSettings(); + + // Wait till the n8n-packages have been read + await loadNodesAndCredentialsPromise; + + // Load the credentials overwrites if any exist + const credentialsOverwrites = CredentialsOverwrites(); + await credentialsOverwrites.init(); + + // Load all external hooks + const externalHooks = ExternalHooks(); + await externalHooks.init(); + + // Add the found types to an instance other parts of the application can use + const nodeTypes = NodeTypes(); + await nodeTypes.init(loadNodesAndCredentials.nodeTypes); + const credentialTypes = CredentialTypes(); + await credentialTypes.init(loadNodesAndCredentials.credentialTypes); + + // Check if the workflow contains the required "Start" node + // "requiredNodeTypes" are also defined in editor-ui/views/NodeView.vue + const requiredNodeTypes = ['n8n-nodes-base.start']; + for (let i = 0; i < allWorkflows.length; i++) { + const workflowData = allWorkflows[i]; + + if (debug) { + this.log(`Starting execution of workflow ID ${workflowData.id}.`); + } + + let startNode: INode | undefined= undefined; + for (const node of workflowData!.nodes) { + if (requiredNodeTypes.includes(node.type)) { + startNode = node; + break; + } + } + + if (startNode === undefined) { + // If the workflow does not contain a start-node we can not know what + // should be executed and with which data to start. + GenericHelpers.logOutput(`Workflow ID ${workflowData.id} cannot be started as it does not contain a "Start" node.`); + continue; + } + + try { + const credentials = await WorkflowCredentials(workflowData!.nodes); + + const runData: IWorkflowExecutionDataProcess = { + credentials, + executionMode: 'cli', + startNodes: [startNode.name], + workflowData: workflowData!, + }; + + const workflowRunner = new WorkflowRunner(); + const executionId = await workflowRunner.run(runData); + + const activeExecutions = ActiveExecutions.getInstance(); + const data = await activeExecutions.getPostExecutePromise(executionId); + + if (data === undefined) { + GenericHelpers.logOutput(`Workflow ${workflowData.id} did not return any data.`); + continue; + } + + if (data.data.resultData.error) { + GenericHelpers.logOutput(`Workflow ${workflowData.id} failed.`); + if (debug) { + this.log(JSON.stringify(data, null, 2)); + console.log(data.data.resultData.error); + } + continue; + } + GenericHelpers.logOutput(`Workflow ${workflowData.id} succeeded.`); + if (flags.snapshot !== undefined) { + const fileName = (flags.snapshot.endsWith(sep) ? flags.snapshot : flags.snapshot + sep) + `${workflowData.id}-snapshot.json`; + fs.writeFileSync(fileName,JSON.stringify(data, null, 2)); + } + } catch (e) { + GenericHelpers.logOutput(`Workflow ${workflowData.id} failed.`); + if (debug) { + console.error(e.message); + console.error(e.stack); + } + } + + } + + this.exit(); + } +} diff --git a/packages/cli/commands/list/workflow.ts b/packages/cli/commands/list/workflow.ts new file mode 100644 index 0000000000..c19e8e4324 --- /dev/null +++ b/packages/cli/commands/list/workflow.ts @@ -0,0 +1,66 @@ +import { + Command, flags, +} from '@oclif/command'; + +import { + IDataObject +} from 'n8n-workflow'; + +import { + Db, +} from "../../src"; + + +export class ListWorkflowCommand extends Command { + static description = '\nList workflows'; + + static examples = [ + `$ n8n list:workflow`, + `$ n8n list:workflow --active=true --onlyId`, + `$ n8n list:workflow --active=false`, + ]; + + static flags = { + help: flags.help({ char: 'h' }), + active: flags.string({ + description: 'Filters workflows by active status. Can be true or false', + }), + onlyId: flags.boolean({ + description: 'Outputs workflow IDs only, one per line.', + }), + }; + + async run() { + const { flags } = this.parse(ListWorkflowCommand); + + if (flags.active !== undefined && !['true', 'false'].includes(flags.active)) { + this.error('The --active flag has to be passed using true or false'); + } + + try { + await Db.init(); + + const findQuery: IDataObject = {}; + if (flags.active !== undefined) { + findQuery.active = flags.active === 'true'; + } + + const workflows = await Db.collections.Workflow!.find(findQuery); + if (flags.onlyId) { + workflows.map(workflow => console.log(workflow.id)); + } else { + workflows.map(workflow => console.log(workflow.id + "|" + workflow.name)); + } + + + } catch (e) { + console.error('\nGOT ERROR'); + console.log('===================================='); + console.error(e.message); + console.error(e.stack); + this.exit(1); + } + + this.exit(); + } +}