From 0430de0eadf67f3df5e4b80133e09b70712168c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Fri, 10 Feb 2023 03:57:15 +0100 Subject: [PATCH 1/2] refactor(core): Delete duplicate code across all commands --- packages/cli/src/ActiveWorkflowRunner.ts | 6 +- packages/cli/src/LoadNodesAndCredentials.ts | 3 +- .../handlers/workflows/workflows.service.ts | 3 +- packages/cli/src/Queue.ts | 3 +- packages/cli/src/WorkflowRunnerProcess.ts | 6 +- packages/cli/src/commands/BaseCommand.ts | 107 +++-- packages/cli/src/commands/Interfaces.d.ts | 12 +- packages/cli/src/commands/audit.ts | 47 +- packages/cli/src/commands/db/revert.ts | 66 +-- packages/cli/src/commands/execute.ts | 158 +++---- packages/cli/src/commands/executeBatch.ts | 155 ++----- .../cli/src/commands/export/credentials.ts | 137 +++--- packages/cli/src/commands/export/workflow.ts | 123 +++--- .../cli/src/commands/import/credentials.ts | 153 ++++--- packages/cli/src/commands/import/workflow.ts | 190 ++++---- packages/cli/src/commands/ldap/reset.ts | 13 +- packages/cli/src/commands/license/clear.ts | 45 +- packages/cli/src/commands/list/workflow.ts | 47 +- packages/cli/src/commands/start.ts | 412 ++++++++---------- packages/cli/src/commands/update/workflow.ts | 55 +-- .../cli/src/commands/user-management/reset.ts | 34 +- packages/cli/src/commands/webhook.ts | 107 +---- packages/cli/src/commands/worker.ts | 366 +++++++--------- packages/cli/src/constants.ts | 2 + packages/cli/src/utils.ts | 3 +- 25 files changed, 947 insertions(+), 1306 deletions(-) diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 14961f2c5ba30..376f6c15e4264 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -64,6 +64,7 @@ import { WorkflowRunner } from '@/WorkflowRunner'; import { ExternalHooks } from '@/ExternalHooks'; import { whereClause } from './UserManagement/UserManagementHelper'; import { WorkflowsService } from './workflows/workflows.services'; +import { START_NODES } from './constants'; const WEBHOOK_PROD_UNREGISTERED_HINT = "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"; @@ -801,10 +802,7 @@ export class ActiveWorkflowRunner { settings: workflowData.settings, }); - const canBeActivated = workflowInstance.checkIfWorkflowCanBeActivated([ - 'n8n-nodes-base.start', - 'n8n-nodes-base.manualTrigger', - ]); + const canBeActivated = workflowInstance.checkIfWorkflowCanBeActivated(START_NODES); if (!canBeActivated) { Logger.error(`Unable to activate workflow "${workflowData.name}"`); throw new Error( diff --git a/packages/cli/src/LoadNodesAndCredentials.ts b/packages/cli/src/LoadNodesAndCredentials.ts index 478b5fe6d88c7..f44c131ad90ae 100644 --- a/packages/cli/src/LoadNodesAndCredentials.ts +++ b/packages/cli/src/LoadNodesAndCredentials.ts @@ -30,6 +30,7 @@ import { RESPONSE_ERROR_MESSAGES, CUSTOM_API_CALL_KEY, CUSTOM_API_CALL_NAME, + inTest, } from '@/constants'; import { persistInstalledPackageData, @@ -61,7 +62,7 @@ export class LoadNodesAndCredentialsClass implements INodesAndCredentials { // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unsafe-call - module.constructor._initPaths(); + if (!inTest) module.constructor._initPaths(); await this.loadNodesFromBasePackages(); await this.loadNodesFromDownloadedPackages(); diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts index 26069da83efc9..62f6d7443880f 100644 --- a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts @@ -11,6 +11,7 @@ import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import { isInstanceOwner } from '../users/users.service'; import type { Role } from '@db/entities/Role'; import config from '@/config'; +import { START_NODES } from '@/constants'; function insertIf(condition: boolean, elements: string[]): string[] { return condition ? elements : []; @@ -128,7 +129,7 @@ export async function updateWorkflow( export function hasStartNode(workflow: WorkflowEntity): boolean { if (!workflow.nodes.length) return false; - const found = workflow.nodes.find((node) => node.type === 'n8n-nodes-base.start'); + const found = workflow.nodes.find((node) => START_NODES.includes(node.type)); return Boolean(found); } diff --git a/packages/cli/src/Queue.ts b/packages/cli/src/Queue.ts index 6f19626dea4d1..88ba6c1a53df2 100644 --- a/packages/cli/src/Queue.ts +++ b/packages/cli/src/Queue.ts @@ -5,6 +5,7 @@ import config from '@/config'; import * as ActiveExecutions from '@/ActiveExecutions'; import * as WebhookHelpers from '@/WebhookHelpers'; +export type JobId = Bull.JobId; export type Job = Bull.Job; export type JobQueue = Bull.Queue; @@ -55,7 +56,7 @@ export class Queue { return this.jobQueue.add(jobData, jobOptions); } - async getJob(jobId: Bull.JobId): Promise { + async getJob(jobId: JobId): Promise { return this.jobQueue.getJob(jobId); } diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index 5beb4c7469c01..1b8b4246ededb 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -101,20 +101,20 @@ class WorkflowRunnerProcess { this.startedAt = new Date(); + const userSettings = await UserSettings.prepareUserSettings(); + const loadNodesAndCredentials = LoadNodesAndCredentials(); await loadNodesAndCredentials.init(); const nodeTypes = NodeTypes(loadNodesAndCredentials); const credentialTypes = CredentialTypes(loadNodesAndCredentials); - - // Load the credentials overwrites if any exist CredentialsOverwrites(credentialTypes); // Load all external hooks const externalHooks = ExternalHooks(); await externalHooks.init(); - const instanceId = (await UserSettings.prepareUserSettings()).instanceId ?? ''; + const instanceId = userSettings.instanceId ?? ''; await InternalHooksManager.init(instanceId, nodeTypes); const binaryDataConfig = config.getEnv('binaryDataManager'); diff --git a/packages/cli/src/commands/BaseCommand.ts b/packages/cli/src/commands/BaseCommand.ts index 445deadfbd9ae..dfaf277ee1858 100644 --- a/packages/cli/src/commands/BaseCommand.ts +++ b/packages/cli/src/commands/BaseCommand.ts @@ -1,59 +1,98 @@ -import { Command } from '@oclif/core'; -import { LoggerProxy } from 'n8n-workflow'; +import { Command } from '@oclif/command'; +import type { INodeTypes } from 'n8n-workflow'; +import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow'; +import type { IUserSettings } from 'n8n-core'; +import { BinaryDataManager, UserSettings } from 'n8n-core'; import type { Logger } from '@/Logger'; import { getLogger } from '@/Logger'; -import { User } from '@db/entities/User'; +import config from '@/config'; import * as Db from '@/Db'; +import * as CrashJournal from '@/CrashJournal'; import { inTest } from '@/constants'; +import { CredentialTypes } from '@/CredentialTypes'; +import { CredentialsOverwrites } from '@/CredentialsOverwrites'; +import { InternalHooksManager } from '@/InternalHooksManager'; +import { initErrorHandling } from '@/ErrorReporting'; +import { ExternalHooks } from '@/ExternalHooks'; +import { NodeTypes } from '@/NodeTypes'; +import type { LoadNodesAndCredentialsClass } from '@/LoadNodesAndCredentials'; +import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import type { IExternalHooksClass } from '@/Interfaces'; + +export const UM_FIX_INSTRUCTION = + 'Please fix the database by running ./packages/cli/bin/n8n user-management:reset'; export abstract class BaseCommand extends Command { - logger: Logger; + protected logger: Logger; + + protected externalHooks: IExternalHooksClass; + + protected loadNodesAndCredentials: LoadNodesAndCredentialsClass; + + protected nodeTypes: INodeTypes; - /** - * Lifecycle methods - */ + protected userSettings: IUserSettings; async init(): Promise { this.logger = getLogger(); LoggerProxy.init(this.logger); - await Db.init(); - } + await initErrorHandling(); - async finally(): Promise { - if (inTest) return; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.stopProcess = this.stopProcess.bind(this); - this.exit(); - } + // Make sure the settings exist + this.userSettings = await UserSettings.prepareUserSettings(); + + this.loadNodesAndCredentials = LoadNodesAndCredentials(); + await this.loadNodesAndCredentials.init(); + this.nodeTypes = NodeTypes(this.loadNodesAndCredentials); + const credentialTypes = CredentialTypes(this.loadNodesAndCredentials); + CredentialsOverwrites(credentialTypes); - /** - * User Management utils - */ + await InternalHooksManager.init(this.userSettings.instanceId ?? '', this.nodeTypes); - defaultUserProps = { - firstName: null, - lastName: null, - email: null, - password: null, - resetPasswordToken: null, - }; + await Db.init().catch(async (error: Error) => + this.exitWithCrash('There was an error initializing DB', error), + ); + } - async getInstanceOwner(): Promise { - const globalRole = await Db.collections.Role.findOneByOrFail({ - name: 'owner', - scope: 'global', - }); + protected async stopProcess() { + // This needs to be overridden + } - const owner = await Db.collections.User.findOneBy({ globalRoleId: globalRole.id }); + protected async initCrashJournal() { + await CrashJournal.init(); + } - if (owner) return owner; + protected async exitSuccessFully() { + try { + await CrashJournal.cleanup(); + } finally { + process.exit(); + } + } - const user = new User(); + protected async exitWithCrash(message: string, error: unknown) { + ErrorReporter.error(new Error(message, { cause: error }), { level: 'fatal' }); + await sleep(2000); + process.exit(1); + } - Object.assign(user, { ...this.defaultUserProps, globalRole }); + protected async initBinaryManager() { + const binaryDataConfig = config.getEnv('binaryDataManager'); + await BinaryDataManager.init(binaryDataConfig, true); + } - await Db.collections.User.save(user); + protected async initExternalHooks() { + this.externalHooks = ExternalHooks(); + await this.externalHooks.init(); + } - return Db.collections.User.findOneByOrFail({ globalRoleId: globalRole.id }); + async finally(error: Error | undefined) { + if (inTest || this.id === 'start') return; + if (Db.isInitialized) await Db.connection.destroy(); + this.exit(error ? 1 : 0); } } diff --git a/packages/cli/src/commands/Interfaces.d.ts b/packages/cli/src/commands/Interfaces.d.ts index 96a31ad9f8969..9f423b575be05 100644 --- a/packages/cli/src/commands/Interfaces.d.ts +++ b/packages/cli/src/commands/Interfaces.d.ts @@ -12,6 +12,7 @@ interface IResult { }; executions: IExecutionResult[]; } + interface IExecutionResult { workflowId: string; workflowName: string; @@ -53,14 +54,3 @@ declare module 'json-diff' { } export function diff(obj1: unknown, obj2: unknown, diffOptions: IDiffOptions): string; } - -type SmtpConfig = { - host: string; - port: number; - secure: boolean; - auth: { - user: string; - pass: string; - }; - sender: string; -}; diff --git a/packages/cli/src/commands/audit.ts b/packages/cli/src/commands/audit.ts index a9f402f01a207..ceac790ea5865 100644 --- a/packages/cli/src/commands/audit.ts +++ b/packages/cli/src/commands/audit.ts @@ -1,19 +1,12 @@ -import Command, { flags } from '@oclif/command'; -import { LoggerProxy } from 'n8n-workflow'; -import { UserSettings } from 'n8n-core'; -import type { Logger } from '@/Logger'; -import { getLogger } from '@/Logger'; +import { flags } from '@oclif/command'; import { audit } from '@/audit'; import { RISK_CATEGORIES } from '@/audit/constants'; -import { CredentialTypes } from '@/CredentialTypes'; -import { NodeTypes } from '@/NodeTypes'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { InternalHooksManager } from '@/InternalHooksManager'; import config from '@/config'; -import * as Db from '@/Db'; import type { Risk } from '@/audit/types'; +import { BaseCommand } from './BaseCommand'; -export class SecurityAudit extends Command { +export class SecurityAudit extends BaseCommand { static description = 'Generate a security audit report for this n8n instance'; static examples = [ @@ -35,11 +28,7 @@ export class SecurityAudit extends Command { }), }; - logger: Logger; - async run() { - await this.init(); - const { flags: auditFlags } = this.parse(SecurityAudit); const categories = @@ -70,38 +59,8 @@ export class SecurityAudit extends Command { void InternalHooksManager.getInstance().onAuditGeneratedViaCli(); } - async init() { - await Db.init(); - - this.initLogger(); - - await this.initInternalHooksManager(); - } - - initLogger() { - this.logger = getLogger(); - LoggerProxy.init(this.logger); - } - - async initInternalHooksManager(): Promise { - const loadNodesAndCredentials = LoadNodesAndCredentials(); - await loadNodesAndCredentials.init(); - - const nodeTypes = NodeTypes(loadNodesAndCredentials); - CredentialTypes(loadNodesAndCredentials); - - const instanceId = await UserSettings.getInstanceId(); - await InternalHooksManager.init(instanceId, nodeTypes); - } - async catch(error: Error) { this.logger.error('Failed to generate security audit'); this.logger.error(error.message); - - this.exit(1); - } - - async finally() { - this.exit(); } } diff --git a/packages/cli/src/commands/db/revert.ts b/packages/cli/src/commands/db/revert.ts index 7ad1964e7db7d..8e1ec167c21ca 100644 --- a/packages/cli/src/commands/db/revert.ts +++ b/packages/cli/src/commands/db/revert.ts @@ -1,10 +1,8 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable no-console */ import { Command, flags } from '@oclif/command'; import type { DataSourceOptions as ConnectionOptions } from 'typeorm'; import { DataSource as Connection } from 'typeorm'; import { LoggerProxy } from 'n8n-workflow'; - +import type { Logger } from '@/Logger'; import { getLogger } from '@/Logger'; import { getConnectionOptions } from '@/Db'; import config from '@/config'; @@ -18,36 +16,42 @@ export class DbRevertMigrationCommand extends Command { help: flags.help({ char: 'h' }), }; - async run() { - const logger = getLogger(); - LoggerProxy.init(logger); + private logger: Logger; + + private connection: Connection; + + async init() { + this.logger = getLogger(); + LoggerProxy.init(this.logger); this.parse(DbRevertMigrationCommand); + } + + async run() { + const dbType = config.getEnv('database.type'); + const connectionOptions: ConnectionOptions = { + ...getConnectionOptions(dbType), + subscribers: [], + synchronize: false, + migrationsRun: false, + dropSchema: false, + logging: ['query', 'error', 'schema'], + }; + + this.connection = new Connection(connectionOptions); + await this.connection.initialize(); + await this.connection.undoLastMigration(); + await this.connection.destroy(); + } + + async catch(error: Error) { + this.logger.error('Error reverting last migration. See log messages for details.'); + this.logger.error(error.message); + } + + protected async finally(error: Error | undefined) { + if (this.connection?.isInitialized) await this.connection.destroy(); - let connection: Connection | undefined; - try { - const dbType = config.getEnv('database.type'); - const connectionOptions: ConnectionOptions = { - ...getConnectionOptions(dbType), - subscribers: [], - synchronize: false, - migrationsRun: false, - dropSchema: false, - logging: ['query', 'error', 'schema'], - }; - connection = new Connection(connectionOptions); - await connection.initialize(); - await connection.undoLastMigration(); - await connection.destroy(); - } catch (error) { - if (connection?.isInitialized) await connection.destroy(); - - console.error('Error reverting last migration. See log messages for details.'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - logger.error(error.message); - this.exit(1); - } - - this.exit(); + this.exit(error ? 1 : 0); } } diff --git a/packages/cli/src/commands/execute.ts b/packages/cli/src/commands/execute.ts index 7b299cde6f21e..6a3d655e03261 100644 --- a/packages/cli/src/commands/execute.ts +++ b/packages/cli/src/commands/execute.ts @@ -1,30 +1,20 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable no-console */ import { promises as fs } from 'fs'; -import { Command, flags } from '@oclif/command'; -import { BinaryDataManager, UserSettings, PLACEHOLDER_EMPTY_WORKFLOW_ID } from 'n8n-core'; +import { flags } from '@oclif/command'; +import { PLACEHOLDER_EMPTY_WORKFLOW_ID } from 'n8n-core'; import type { IWorkflowBase } from 'n8n-workflow'; -import { LoggerProxy } from 'n8n-workflow'; +import { ExecutionBaseError } from 'n8n-workflow'; import * as ActiveExecutions from '@/ActiveExecutions'; -import { CredentialsOverwrites } from '@/CredentialsOverwrites'; -import { CredentialTypes } from '@/CredentialTypes'; import * as Db from '@/Db'; -import { ExternalHooks } from '@/ExternalHooks'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; -import { NodeTypes } from '@/NodeTypes'; -import { InternalHooksManager } from '@/InternalHooksManager'; import * as WorkflowHelpers from '@/WorkflowHelpers'; import { WorkflowRunner } from '@/WorkflowRunner'; import type { IWorkflowExecutionDataProcess } from '@/Interfaces'; -import { getLogger } from '@/Logger'; -import config from '@/config'; import { getInstanceOwner } from '@/UserManagement/UserManagementHelper'; import { findCliWorkflowStart } from '@/utils'; import { initEvents } from '@/events'; +import { BaseCommand } from './BaseCommand'; -export class Execute extends Command { +export class Execute extends BaseCommand { static description = '\nExecutes a given workflow'; static examples = ['$ n8n execute --id=5', '$ n8n execute --file=workflow.json']; @@ -42,33 +32,26 @@ export class Execute extends Command { }), }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async run() { - const logger = getLogger(); - LoggerProxy.init(logger); - const binaryDataConfig = config.getEnv('binaryDataManager'); - await BinaryDataManager.init(binaryDataConfig, true); + async init() { + await super.init(); + await this.initBinaryManager(); + await this.initExternalHooks(); // Add event handlers initEvents(); + } + async run() { // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(Execute); - // 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(); - if (!flags.id && !flags.file) { - console.info('Either option "--id" or "--file" have to be set!'); + this.logger.info('Either option "--id" or "--file" have to be set!'); return; } if (flags.id && flags.file) { - console.info('Either "id" or "file" can be set never both!'); + this.logger.info('Either "id" or "file" can be set never both!'); return; } @@ -82,7 +65,7 @@ export class Execute extends Command { } catch (error) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (error.code === 'ENOENT') { - console.info(`The file "${flags.file}" could not be found.`); + this.logger.info(`The file "${flags.file}" could not be found.`); return; } @@ -96,22 +79,19 @@ export class Execute extends Command { workflowData.nodes === undefined || workflowData.connections === undefined ) { - console.info(`The file "${flags.file}" does not contain valid workflow data.`); + this.logger.info(`The file "${flags.file}" does not contain valid workflow data.`); return; } workflowId = workflowData.id ?? PLACEHOLDER_EMPTY_WORKFLOW_ID; } - // Wait till the database is ready - await startDbInitPromise; - if (flags.id) { // Id of workflow is given workflowId = flags.id; workflowData = await Db.collections.Workflow.findOneBy({ id: workflowId }); if (workflowData === null) { - console.info(`The workflow with the id "${workflowId}" does not exist.`); + this.logger.info(`The workflow with the id "${workflowId}" does not exist.`); process.exit(1); } } @@ -120,82 +100,56 @@ export class Execute extends Command { throw new Error('Failed to retrieve workflow data for requested workflow'); } - // Make sure the settings exist - await UserSettings.prepareUserSettings(); - - // Wait till the n8n-packages have been read - await loadNodesAndCredentialsPromise; - - NodeTypes(loadNodesAndCredentials); - const credentialTypes = CredentialTypes(loadNodesAndCredentials); - - // Load the credentials overwrites if any exist - CredentialsOverwrites(credentialTypes); - - // 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(loadNodesAndCredentials); - CredentialTypes(loadNodesAndCredentials); - - const instanceId = await UserSettings.getInstanceId(); - await InternalHooksManager.init(instanceId, nodeTypes); - if (!WorkflowHelpers.isWorkflowIdValid(workflowId)) { workflowId = undefined; } - try { - const startingNode = findCliWorkflowStart(workflowData.nodes); + const startingNode = findCliWorkflowStart(workflowData.nodes); - const user = await getInstanceOwner(); - const runData: IWorkflowExecutionDataProcess = { - executionMode: 'cli', - startNodes: [startingNode.name], - workflowData, - userId: user.id, - }; + const user = await getInstanceOwner(); + const runData: IWorkflowExecutionDataProcess = { + executionMode: 'cli', + startNodes: [startingNode.name], + workflowData, + userId: user.id, + }; - const workflowRunner = new WorkflowRunner(); - const executionId = await workflowRunner.run(runData); + const workflowRunner = new WorkflowRunner(); + const executionId = await workflowRunner.run(runData); - const activeExecutions = ActiveExecutions.getInstance(); - const data = await activeExecutions.getPostExecutePromise(executionId); + const activeExecutions = ActiveExecutions.getInstance(); + const data = await activeExecutions.getPostExecutePromise(executionId); - if (data === undefined) { - throw new Error('Workflow did not return any data!'); - } + if (data === undefined) { + throw new Error('Workflow did not return any data!'); + } - if (data.data.resultData.error) { - console.info('Execution was NOT successful. See log message for details.'); - logger.info('Execution error:'); - logger.info('===================================='); - logger.info(JSON.stringify(data, null, 2)); - - const { error } = data.data.resultData; - // eslint-disable-next-line @typescript-eslint/no-throw-literal - throw { - ...error, - stack: error.stack, - }; - } - if (flags.rawOutput === undefined) { - this.log('Execution was successful:'); - this.log('===================================='); - } - this.log(JSON.stringify(data, null, 2)); - } catch (e) { - console.error('Error executing workflow. See log messages for details.'); - logger.error('\nExecution error:'); - logger.info('===================================='); - logger.error(e.message); - if (e.description) logger.error(e.description); - logger.error(e.stack); - this.exit(1); + if (data.data.resultData.error) { + this.logger.info('Execution was NOT successful. See log message for details.'); + this.logger.info('Execution error:'); + this.logger.info('===================================='); + this.logger.info(JSON.stringify(data, null, 2)); + + const { error } = data.data.resultData; + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw { + ...error, + stack: error.stack, + }; } + if (flags.rawOutput === undefined) { + this.log('Execution was successful:'); + this.log('===================================='); + } + this.log(JSON.stringify(data, null, 2)); + } - this.exit(); + async catch(error: Error) { + this.logger.error('Error executing workflow. See log messages for details.'); + this.logger.error('\nExecution error:'); + this.logger.info('===================================='); + this.logger.error(error.message); + if (error instanceof ExecutionBaseError) this.logger.error(error.description!); + this.logger.error(error.stack!); } } diff --git a/packages/cli/src/commands/executeBatch.ts b/packages/cli/src/commands/executeBatch.ts index 61021746d1736..592c1873c683b 100644 --- a/packages/cli/src/commands/executeBatch.ts +++ b/packages/cli/src/commands/executeBatch.ts @@ -1,45 +1,25 @@ -/* eslint-disable @typescript-eslint/prefer-optional-chain */ -/* eslint-disable array-callback-return */ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -/* eslint-disable no-await-in-loop */ -/* eslint-disable no-async-promise-executor */ -/* eslint-disable no-param-reassign */ -/* eslint-disable @typescript-eslint/unbound-method */ -/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-loop-func */ import fs from 'fs'; -import { Command, flags } from '@oclif/command'; - -import { BinaryDataManager, UserSettings } from 'n8n-core'; - +import { flags } from '@oclif/command'; import type { ITaskData } from 'n8n-workflow'; -import { LoggerProxy, sleep } from 'n8n-workflow'; - +import { sleep } from 'n8n-workflow'; import { sep } from 'path'; - import { diff } from 'json-diff'; - import pick from 'lodash.pick'; -import { getLogger } from '@/Logger'; import * as ActiveExecutions from '@/ActiveExecutions'; -import { CredentialsOverwrites } from '@/CredentialsOverwrites'; -import { CredentialTypes } from '@/CredentialTypes'; import * as Db from '@/Db'; -import { ExternalHooks } from '@/ExternalHooks'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; -import { NodeTypes } from '@/NodeTypes'; -import { InternalHooksManager } from '@/InternalHooksManager'; import { WorkflowRunner } from '@/WorkflowRunner'; import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces'; -import config from '@/config'; import type { User } from '@db/entities/User'; import { getInstanceOwner } from '@/UserManagement/UserManagementHelper'; import { findCliWorkflowStart } from '@/utils'; import { initEvents } from '@/events'; +import { BaseCommand } from './BaseCommand'; const re = /\d+/; -export class ExecuteBatch extends Command { +export class ExecuteBatch extends BaseCommand { static description = '\nExecutes multiple workflows once'; static cancelled = false; @@ -115,7 +95,6 @@ export class ExecuteBatch extends Command { * Gracefully handles exit. * @param {boolean} skipExit Whether to skip exit or number according to received signal */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types static async stopProcess(skipExit: boolean | number = false) { if (ExecuteBatch.cancelled) { process.exit(0); @@ -123,16 +102,13 @@ export class ExecuteBatch extends Command { ExecuteBatch.cancelled = true; const activeExecutionsInstance = ActiveExecutions.getInstance(); - const stopPromises = activeExecutionsInstance.getActiveExecutions().map(async (execution) => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - activeExecutionsInstance.stopExecution(execution.id); - }); + const stopPromises = activeExecutionsInstance + .getActiveExecutions() + .map(async (execution) => activeExecutionsInstance.stopExecution(execution.id)); await Promise.allSettled(stopPromises); - setTimeout(() => { - process.exit(0); - }, 30000); + setTimeout(() => process.exit(0), 30000); let executingWorkflows = activeExecutionsInstance.getActiveExecutions(); @@ -144,7 +120,6 @@ export class ExecuteBatch extends Command { console.log(` - Execution ID ${execution.id}, workflow ID: ${execution.workflowId}`); }); } - // eslint-disable-next-line no-await-in-loop await sleep(500); executingWorkflows = activeExecutionsInstance.getActiveExecutions(); } @@ -155,13 +130,11 @@ export class ExecuteBatch extends Command { } } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - formatJsonOutput(data: object) { + private formatJsonOutput(data: object) { return JSON.stringify(data, null, 2); } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - shouldBeConsideredAsWarning(errorMessage: string) { + private shouldBeConsideredAsWarning(errorMessage: string) { const warningStrings = [ 'refresh token is invalid', 'unable to connect to', @@ -174,7 +147,6 @@ export class ExecuteBatch extends Command { 'status code 401', ]; - // eslint-disable-next-line no-param-reassign errorMessage = errorMessage.toLowerCase(); for (let i = 0; i < warningStrings.length; i++) { @@ -186,22 +158,24 @@ export class ExecuteBatch extends Command { return false; } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async init() { + await super.init(); + await this.initBinaryManager(); + await this.initExternalHooks(); + + // Add event handlers + initEvents(); + } + async run() { + // eslint-disable-next-line @typescript-eslint/unbound-method process.once('SIGTERM', ExecuteBatch.stopProcess); + // eslint-disable-next-line @typescript-eslint/unbound-method process.once('SIGINT', ExecuteBatch.stopProcess); - const logger = getLogger(); - LoggerProxy.init(logger); - const binaryDataConfig = config.getEnv('binaryDataManager'); - await BinaryDataManager.init(binaryDataConfig, true); - // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(ExecuteBatch); - // Add event handlers - initEvents(); - ExecuteBatch.debug = flags.debug; ExecuteBatch.concurrency = flags.concurrency || 1; @@ -277,23 +251,8 @@ export class ExecuteBatch extends Command { ExecuteBatch.shallow = true; } - // 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(); - - // Make sure the settings exist - await UserSettings.prepareUserSettings(); - - // Wait till the database is ready - await startDbInitPromise; - ExecuteBatch.instanceOwner = await getInstanceOwner(); - let allWorkflows; - const query = Db.collections.Workflow.createQueryBuilder('workflows'); if (ids.length > 0) { @@ -304,33 +263,12 @@ export class ExecuteBatch extends Command { query.andWhere('workflows.id not in (:...skipIds)', { skipIds }); } - // eslint-disable-next-line prefer-const - allWorkflows = (await query.getMany()) as IWorkflowDb[]; + const allWorkflows = (await query.getMany()) as IWorkflowDb[]; if (ExecuteBatch.debug) { process.stdout.write(`Found ${allWorkflows.length} workflows to execute.\n`); } - // Wait till the n8n-packages have been read - await loadNodesAndCredentialsPromise; - - NodeTypes(loadNodesAndCredentials); - const credentialTypes = CredentialTypes(loadNodesAndCredentials); - - // Load the credentials overwrites if any exist - CredentialsOverwrites(credentialTypes); - - // 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(loadNodesAndCredentials); - CredentialTypes(loadNodesAndCredentials); - - const instanceId = await UserSettings.getInstanceId(); - await InternalHooksManager.init(instanceId, nodeTypes); - // Send a shallow copy of allWorkflows so we still have all workflow data. const results = await this.runTests([...allWorkflows]); @@ -348,7 +286,6 @@ export class ExecuteBatch extends Command { failedWorkflowIds.includes(workflow.id), ); - // eslint-disable-next-line no-await-in-loop const retryResults = await this.runTests(newWorkflowList); this.mergeResults(results, retryResults); @@ -389,7 +326,6 @@ export class ExecuteBatch extends Command { this.exit(0); } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types mergeResults(results: IResult, retryResults: IResult) { if (retryResults.summary.successfulExecutions === 0) { // Nothing to replace. @@ -430,7 +366,7 @@ export class ExecuteBatch extends Command { }); } - async runTests(allWorkflows: IWorkflowDb[]): Promise { + private async runTests(allWorkflows: IWorkflowDb[]): Promise { const result: IResult = { totalWorkflows: allWorkflows.length, summary: { @@ -475,7 +411,6 @@ export class ExecuteBatch extends Command { this.updateStatus(); } - // eslint-disable-next-line @typescript-eslint/no-loop-func await this.startThread(workflow).then((executionResult) => { if (ExecuteBatch.debug) { ExecuteBatch.workflowExecutionsProgress[i].pop(); @@ -542,7 +477,6 @@ export class ExecuteBatch extends Command { }); } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types updateStatus() { if (ExecuteBatch.cancelled) { return; @@ -584,7 +518,6 @@ export class ExecuteBatch extends Command { }); } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types initializeLogs() { process.stdout.write('**********************************************\n'); process.stdout.write(' n8n test workflows\n'); @@ -687,12 +620,13 @@ export class ExecuteBatch extends Command { 1000; executionResult.finished = data?.finished !== undefined; - if (data.data.resultData.error) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-prototype-builtins - executionResult.error = data.data.resultData.error.hasOwnProperty('description') + const resultError = data.data.resultData.error; + if (resultError) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + executionResult.error = resultError.hasOwnProperty('description') ? // @ts-ignore - data.data.resultData.error.description - : data.data.resultData.error.message; + resultError.description + : resultError.message; if (data.data.resultData.lastNodeExecuted !== undefined) { executionResult.error += ` on node ${data.data.resultData.lastNodeExecuted}`; } @@ -723,33 +657,29 @@ export class ExecuteBatch extends Command { return; } - if ( - nodeEdgeCases[nodeName] !== undefined && - nodeEdgeCases[nodeName].capResults !== undefined - ) { - executionDataArray.splice(nodeEdgeCases[nodeName].capResults!); + const { capResults, ignoredProperties, keepOnlyProperties } = + nodeEdgeCases[nodeName] || {}; + + if (capResults !== undefined) { + executionDataArray.splice(capResults); } executionDataArray.map((executionData) => { if (executionData.json === undefined) { return; } - if ( - nodeEdgeCases[nodeName] !== undefined && - nodeEdgeCases[nodeName].ignoredProperties !== undefined - ) { - nodeEdgeCases[nodeName].ignoredProperties!.forEach( + + if (ignoredProperties !== undefined) { + ignoredProperties.forEach( (ignoredProperty) => delete executionData.json[ignoredProperty], ); } let keepOnlyFields = [] as string[]; - if ( - nodeEdgeCases[nodeName] !== undefined && - nodeEdgeCases[nodeName].keepOnlyProperties !== undefined - ) { - keepOnlyFields = nodeEdgeCases[nodeName].keepOnlyProperties!; + if (keepOnlyProperties !== undefined) { + keepOnlyFields = keepOnlyProperties; } + executionData.json = keepOnlyFields.length > 0 ? pick(executionData.json, keepOnlyFields) @@ -857,8 +787,7 @@ export class ExecuteBatch extends Command { } } } catch (e) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access - executionResult.error = `Workflow failed to execute: ${e.message}`; + executionResult.error = `Workflow failed to execute: ${(e as Error).message}`; executionResult.executionStatus = 'error'; } clearTimeout(timeoutTimer); diff --git a/packages/cli/src/commands/export/credentials.ts b/packages/cli/src/commands/export/credentials.ts index 22ce61aff5bbd..d19dd80953425 100644 --- a/packages/cli/src/commands/export/credentials.ts +++ b/packages/cli/src/commands/export/credentials.ts @@ -1,21 +1,13 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-plus-operands */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable no-console */ -import { Command, flags } from '@oclif/command'; - -import { Credentials, UserSettings } from 'n8n-core'; - -import type { IDataObject } from 'n8n-workflow'; -import { LoggerProxy } from 'n8n-workflow'; - +import { flags } from '@oclif/command'; import fs from 'fs'; import path from 'path'; -import { getLogger } from '@/Logger'; +import { Credentials, UserSettings } from 'n8n-core'; +import type { IDataObject } from 'n8n-workflow'; import * as Db from '@/Db'; import type { ICredentialsDecryptedDb } from '@/Interfaces'; +import { BaseCommand } from '../BaseCommand'; -export class ExportCredentialsCommand extends Command { +export class ExportCredentialsCommand extends BaseCommand { static description = 'Export credentials'; static examples = [ @@ -55,11 +47,7 @@ export class ExportCredentialsCommand extends Command { }), }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async run() { - const logger = getLogger(); - LoggerProxy.init(logger); - // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(ExportCredentialsCommand); @@ -70,104 +58,103 @@ export class ExportCredentialsCommand extends Command { } if (!flags.all && !flags.id) { - console.info('Either option "--all" or "--id" have to be set!'); + this.logger.info('Either option "--all" or "--id" have to be set!'); return; } if (flags.all && flags.id) { - console.info('You should either use "--all" or "--id" but never both!'); + this.logger.info('You should either use "--all" or "--id" but never both!'); return; } if (flags.separate) { try { if (!flags.output) { - console.info('You must inform an output directory via --output when using --separate'); + this.logger.info( + 'You must inform an output directory via --output when using --separate', + ); return; } if (fs.existsSync(flags.output)) { if (!fs.lstatSync(flags.output).isDirectory()) { - console.info('The parameter --output must be a directory'); + this.logger.info('The parameter --output must be a directory'); return; } } else { fs.mkdirSync(flags.output, { recursive: true }); } } catch (e) { - console.error( + this.logger.error( 'Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.', ); - logger.error('\nFILESYSTEM ERROR'); - logger.info('===================================='); - logger.error(e.message); - logger.error(e.stack); - this.exit(1); + this.logger.error('\nFILESYSTEM ERROR'); + if (e instanceof Error) { + this.logger.info('===================================='); + this.logger.error(e.message); + this.logger.error(e.stack!); + } + return; } } else if (flags.output) { if (fs.existsSync(flags.output)) { if (fs.lstatSync(flags.output).isDirectory()) { - console.info('The parameter --output must be a writeable file'); + this.logger.info('The parameter --output must be a writeable file'); return; } } } - try { - await Db.init(); - - const findQuery: IDataObject = {}; - if (flags.id) { - findQuery.id = flags.id; - } + const findQuery: IDataObject = {}; + if (flags.id) { + findQuery.id = flags.id; + } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const credentials = await Db.collections.Credentials.find(findQuery); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const credentials = await Db.collections.Credentials.find(findQuery); - if (flags.decrypted) { - const encryptionKey = await UserSettings.getEncryptionKey(); + if (flags.decrypted) { + const encryptionKey = await UserSettings.getEncryptionKey(); - for (let i = 0; i < credentials.length; i++) { - const { name, type, nodesAccess, data } = credentials[i]; - const id = credentials[i].id; - const credential = new Credentials({ id, name }, type, nodesAccess, data); - const plainData = credential.getData(encryptionKey); - (credentials[i] as ICredentialsDecryptedDb).data = plainData; - } + for (let i = 0; i < credentials.length; i++) { + const { name, type, nodesAccess, data } = credentials[i]; + const id = credentials[i].id; + const credential = new Credentials({ id, name }, type, nodesAccess, data); + const plainData = credential.getData(encryptionKey); + (credentials[i] as ICredentialsDecryptedDb).data = plainData; } + } - if (credentials.length === 0) { - throw new Error('No credentials found with specified filters.'); - } + if (credentials.length === 0) { + throw new Error('No credentials found with specified filters.'); + } - if (flags.separate) { - let fileContents: string; - let i: number; - for (i = 0; i < credentials.length; i++) { - fileContents = JSON.stringify(credentials[i], null, flags.pretty ? 2 : undefined); - const filename = `${ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + - credentials[i].id - }.json`; - fs.writeFileSync(filename, fileContents); - } - console.info(`Successfully exported ${i} credentials.`); + if (flags.separate) { + let fileContents: string; + let i: number; + for (i = 0; i < credentials.length; i++) { + fileContents = JSON.stringify(credentials[i], null, flags.pretty ? 2 : undefined); + const filename = `${ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/restrict-plus-operands + (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + + credentials[i].id + }.json`; + fs.writeFileSync(filename, fileContents); + } + this.logger.info(`Successfully exported ${i} credentials.`); + } else { + const fileContents = JSON.stringify(credentials, null, flags.pretty ? 2 : undefined); + if (flags.output) { + fs.writeFileSync(flags.output, fileContents); + this.logger.info(`Successfully exported ${credentials.length} credentials.`); } else { - const fileContents = JSON.stringify(credentials, null, flags.pretty ? 2 : undefined); - if (flags.output) { - fs.writeFileSync(flags.output, fileContents); - console.info(`Successfully exported ${credentials.length} credentials.`); - } else { - console.info(fileContents); - } + this.logger.info(fileContents); } - // Force exit as process won't exit using MySQL or Postgres. - process.exit(0); - } catch (error) { - console.error('Error exporting credentials. See log messages for details.'); - logger.error(error.message); - this.exit(1); } } + + async catch(error: Error) { + this.logger.error('Error exporting credentials. See log messages for details.'); + this.logger.error(error.message); + } } diff --git a/packages/cli/src/commands/export/workflow.ts b/packages/cli/src/commands/export/workflow.ts index 14b46cbcd9653..6a3d3cdf52d55 100644 --- a/packages/cli/src/commands/export/workflow.ts +++ b/packages/cli/src/commands/export/workflow.ts @@ -1,17 +1,11 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable no-console */ -import { Command, flags } from '@oclif/command'; - -import type { IDataObject } from 'n8n-workflow'; -import { LoggerProxy } from 'n8n-workflow'; - +import { flags } from '@oclif/command'; import fs from 'fs'; import path from 'path'; -import { getLogger } from '@/Logger'; +import type { IDataObject } from 'n8n-workflow'; import * as Db from '@/Db'; +import { BaseCommand } from '../BaseCommand'; -export class ExportWorkflowsCommand extends Command { +export class ExportWorkflowsCommand extends BaseCommand { static description = 'Export workflows'; static examples = [ @@ -46,11 +40,7 @@ export class ExportWorkflowsCommand extends Command { }), }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async run() { - const logger = getLogger(); - LoggerProxy.init(logger); - // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(ExportWorkflowsCommand); @@ -61,98 +51,97 @@ export class ExportWorkflowsCommand extends Command { } if (!flags.all && !flags.id) { - console.info('Either option "--all" or "--id" have to be set!'); + this.logger.info('Either option "--all" or "--id" have to be set!'); return; } if (flags.all && flags.id) { - console.info('You should either use "--all" or "--id" but never both!'); + this.logger.info('You should either use "--all" or "--id" but never both!'); return; } if (flags.separate) { try { if (!flags.output) { - console.info('You must inform an output directory via --output when using --separate'); + this.logger.info( + 'You must inform an output directory via --output when using --separate', + ); return; } if (fs.existsSync(flags.output)) { if (!fs.lstatSync(flags.output).isDirectory()) { - console.info('The parameter --output must be a directory'); + this.logger.info('The parameter --output must be a directory'); return; } } else { fs.mkdirSync(flags.output, { recursive: true }); } } catch (e) { - console.error( + this.logger.error( 'Aborting execution as a filesystem error has been encountered while creating the output directory. See log messages for details.', ); - logger.error('\nFILESYSTEM ERROR'); - logger.info('===================================='); - logger.error(e.message); - logger.error(e.stack); + this.logger.error('\nFILESYSTEM ERROR'); + this.logger.info('===================================='); + if (e instanceof Error) { + this.logger.error(e.message); + this.logger.error(e.stack!); + } this.exit(1); } } else if (flags.output) { if (fs.existsSync(flags.output)) { if (fs.lstatSync(flags.output).isDirectory()) { - console.info('The parameter --output must be a writeable file'); + this.logger.info('The parameter --output must be a writeable file'); return; } } } - try { - await Db.init(); + const findQuery: IDataObject = {}; + if (flags.id) { + findQuery.id = flags.id; + } - const findQuery: IDataObject = {}; - if (flags.id) { - findQuery.id = flags.id; - } + const workflows = await Db.collections.Workflow.find({ + where: findQuery, + relations: ['tags'], + }); - const workflows = await Db.collections.Workflow.find({ - where: findQuery, - relations: ['tags'], - }); + if (workflows.length === 0) { + throw new Error('No workflows found with specified filters.'); + } - if (workflows.length === 0) { - throw new Error('No workflows found with specified filters.'); + if (flags.separate) { + let fileContents: string; + let i: number; + for (i = 0; i < workflows.length; i++) { + fileContents = JSON.stringify(workflows[i], null, flags.pretty ? 2 : undefined); + const filename = `${ + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-non-null-assertion + (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + + workflows[i].id + }.json`; + fs.writeFileSync(filename, fileContents); } - - if (flags.separate) { - let fileContents: string; - let i: number; - for (i = 0; i < workflows.length; i++) { - fileContents = JSON.stringify(workflows[i], null, flags.pretty ? 2 : undefined); - const filename = `${ - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-non-null-assertion - (flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) + - workflows[i].id - }.json`; - fs.writeFileSync(filename, fileContents); - } - console.info(`Successfully exported ${i} workflows.`); + this.logger.info(`Successfully exported ${i} workflows.`); + } else { + const fileContents = JSON.stringify(workflows, null, flags.pretty ? 2 : undefined); + if (flags.output) { + fs.writeFileSync(flags.output, fileContents); + this.logger.info( + `Successfully exported ${workflows.length} ${ + workflows.length === 1 ? 'workflow.' : 'workflows.' + }`, + ); } else { - const fileContents = JSON.stringify(workflows, null, flags.pretty ? 2 : undefined); - if (flags.output) { - fs.writeFileSync(flags.output, fileContents); - console.info( - `Successfully exported ${workflows.length} ${ - workflows.length === 1 ? 'workflow.' : 'workflows.' - }`, - ); - } else { - console.info(fileContents); - } + this.logger.info(fileContents); } - // Force exit as process won't exit using MySQL or Postgres. - process.exit(0); - } catch (error) { - console.error('Error exporting workflows. See log messages for details.'); - logger.error(error.message); - this.exit(1); } } + + async catch(error: Error) { + this.logger.error('Error exporting workflows. See log messages for details.'); + this.logger.error(error.message); + } } diff --git a/packages/cli/src/commands/import/credentials.ts b/packages/cli/src/commands/import/credentials.ts index be0e2de7bd3ee..efed0a972cab5 100644 --- a/packages/cli/src/commands/import/credentials.ts +++ b/packages/cli/src/commands/import/credentials.ts @@ -1,18 +1,8 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable no-restricted-syntax */ -/* eslint-disable @typescript-eslint/no-shadow */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable no-await-in-loop */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable no-console */ -import { Command, flags } from '@oclif/command'; -import { Credentials, UserSettings } from 'n8n-core'; -import { LoggerProxy } from 'n8n-workflow'; +import { flags } from '@oclif/command'; +import { Credentials } from 'n8n-core'; import fs from 'fs'; import glob from 'fast-glob'; import type { EntityManager } from 'typeorm'; -import { getLogger } from '@/Logger'; import config from '@/config'; import * as Db from '@/Db'; import type { User } from '@db/entities/User'; @@ -20,11 +10,11 @@ import { SharedCredentials } from '@db/entities/SharedCredentials'; import type { Role } from '@db/entities/Role'; import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { disableAutoGeneratedIds } from '@db/utils/commandHelpers'; +import { BaseCommand, UM_FIX_INSTRUCTION } from '../BaseCommand'; +import type { ICredentialsEncrypted } from 'n8n-workflow'; +import { jsonParse } from 'n8n-workflow'; -const FIX_INSTRUCTION = - 'Please fix the database by running ./packages/cli/bin/n8n user-management:reset'; - -export class ImportCredentialsCommand extends Command { +export class ImportCredentialsCommand extends BaseCommand { static description = 'Import credentials'; static examples = [ @@ -48,25 +38,28 @@ export class ImportCredentialsCommand extends Command { }), }; - ownerCredentialRole: Role; + private ownerCredentialRole: Role; - transactionManager: EntityManager; + private transactionManager: EntityManager; - async run(): Promise { - const logger = getLogger(); - LoggerProxy.init(logger); + async init() { + disableAutoGeneratedIds(CredentialsEntity); + await super.init(); + } + async run(): Promise { + // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(ImportCredentialsCommand); if (!flags.input) { - console.info('An input file or directory with --input must be provided'); + this.logger.info('An input file or directory with --input must be provided'); return; } if (flags.separate) { if (fs.existsSync(flags.input)) { if (!fs.lstatSync(flags.input).isDirectory()) { - console.info('The argument to --input must be a directory'); + this.logger.info('The argument to --input must be a directory'); return; } } @@ -74,82 +67,82 @@ export class ImportCredentialsCommand extends Command { let totalImported = 0; - try { - disableAutoGeneratedIds(CredentialsEntity); - await Db.init(); - - await this.initOwnerCredentialRole(); - const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner(); - - // Make sure the settings exist - await UserSettings.prepareUserSettings(); - - const encryptionKey = await UserSettings.getEncryptionKey(); - - if (flags.separate) { - let { input: inputPath } = flags; - - if (process.platform === 'win32') { - inputPath = inputPath.replace(/\\/g, '/'); - } - - const files = await glob('*.json', { - cwd: inputPath, - absolute: true, - }); - - totalImported = files.length; + await this.initOwnerCredentialRole(); + const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner(); - await Db.getConnection().transaction(async (transactionManager) => { - this.transactionManager = transactionManager; - for (const file of files) { - const credential = JSON.parse(fs.readFileSync(file, { encoding: 'utf8' })); + const encryptionKey = this.userSettings.encryptionKey; - if (typeof credential.data === 'object') { - // plain data / decrypted input. Should be encrypted first. - Credentials.prototype.setData.call(credential, credential.data, encryptionKey); - } - - await this.storeCredential(credential, user); - } - }); + if (flags.separate) { + let { input: inputPath } = flags; - this.reportSuccess(totalImported); - process.exit(); + if (process.platform === 'win32') { + inputPath = inputPath.replace(/\\/g, '/'); } - const credentials = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' })); - - totalImported = credentials.length; + const files = await glob('*.json', { + cwd: inputPath, + absolute: true, + }); - if (!Array.isArray(credentials)) { - throw new Error( - 'File does not seem to contain credentials. Make sure the credentials are contained in an array.', - ); - } + totalImported = files.length; await Db.getConnection().transaction(async (transactionManager) => { this.transactionManager = transactionManager; - for (const credential of credentials) { + for (const file of files) { + const credential = jsonParse( + fs.readFileSync(file, { encoding: 'utf8' }), + ); + if (typeof credential.data === 'object') { // plain data / decrypted input. Should be encrypted first. Credentials.prototype.setData.call(credential, credential.data, encryptionKey); } + await this.storeCredential(credential, user); } }); this.reportSuccess(totalImported); - process.exit(); - } catch (error) { - console.error('An error occurred while importing credentials. See log messages for details.'); - if (error instanceof Error) logger.error(error.message); - this.exit(1); + return; + } + + const credentials = jsonParse( + fs.readFileSync(flags.input, { encoding: 'utf8' }), + ); + + totalImported = credentials.length; + + if (!Array.isArray(credentials)) { + throw new Error( + 'File does not seem to contain credentials. Make sure the credentials are contained in an array.', + ); } + + await Db.getConnection().transaction(async (transactionManager) => { + this.transactionManager = transactionManager; + for (const credential of credentials) { + if (typeof credential.data === 'object') { + // plain data / decrypted input. Should be encrypted first. + Credentials.prototype.setData.call(credential, credential.data, encryptionKey); + } + await this.storeCredential(credential, user); + } + }); + + this.reportSuccess(totalImported); + } + + async catch(error: Error) { + this.logger.error( + 'An error occurred while importing credentials. See log messages for details.', + ); + this.logger.error(error.message); } private reportSuccess(total: number) { - console.info(`Successfully imported ${total} ${total === 1 ? 'credential.' : 'credentials.'}`); + this.logger.info( + `Successfully imported ${total} ${total === 1 ? 'credential.' : 'credentials.'}`, + ); } private async initOwnerCredentialRole() { @@ -158,7 +151,7 @@ export class ImportCredentialsCommand extends Command { }); if (!ownerCredentialRole) { - throw new Error(`Failed to find owner credential role. ${FIX_INSTRUCTION}`); + throw new Error(`Failed to find owner credential role. ${UM_FIX_INSTRUCTION}`); } this.ownerCredentialRole = ownerCredentialRole; @@ -169,7 +162,7 @@ export class ImportCredentialsCommand extends Command { await this.transactionManager.upsert( SharedCredentials, { - credentialsId: result.identifiers[0].id, + credentialsId: result.identifiers[0].id as string, userId: user.id, roleId: this.ownerCredentialRole.id, }, @@ -192,7 +185,7 @@ export class ImportCredentialsCommand extends Command { (await Db.collections.User.findOneBy({ globalRoleId: ownerGlobalRole.id })); if (!owner) { - throw new Error(`Failed to find owner. ${FIX_INSTRUCTION}`); + throw new Error(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); } return owner; diff --git a/packages/cli/src/commands/import/workflow.ts b/packages/cli/src/commands/import/workflow.ts index 027c77bc58045..df93f7f79fac9 100644 --- a/packages/cli/src/commands/import/workflow.ts +++ b/packages/cli/src/commands/import/workflow.ts @@ -1,23 +1,10 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable no-restricted-syntax */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-shadow */ -/* eslint-disable @typescript-eslint/no-loop-func */ -/* eslint-disable no-await-in-loop */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -/* eslint-disable no-console */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { Command, flags } from '@oclif/command'; +import { flags } from '@oclif/command'; import type { INode, INodeCredentialsDetails } from 'n8n-workflow'; -import { LoggerProxy } from 'n8n-workflow'; +import { jsonParse } from 'n8n-workflow'; import fs from 'fs'; import glob from 'fast-glob'; -import { UserSettings } from 'n8n-core'; import type { EntityManager } from 'typeorm'; import { v4 as uuid } from 'uuid'; -import { getLogger } from '@/Logger'; import config from '@/config'; import * as Db from '@/Db'; import { SharedWorkflow } from '@db/entities/SharedWorkflow'; @@ -28,9 +15,7 @@ import { setTagsForImport } from '@/TagHelpers'; import type { ICredentialsDb, IWorkflowToImport } from '@/Interfaces'; import { disableAutoGeneratedIds } from '@db/utils/commandHelpers'; import { replaceInvalidCredentials } from '@/WorkflowHelpers'; - -const FIX_INSTRUCTION = - 'Please fix the database by running ./packages/cli/bin/n8n user-management:reset'; +import { BaseCommand, UM_FIX_INSTRUCTION } from '../BaseCommand'; function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] { if (!Array.isArray(workflows)) { @@ -50,7 +35,7 @@ function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IW } } -export class ImportWorkflowsCommand extends Command { +export class ImportWorkflowsCommand extends BaseCommand { static description = 'Import workflows'; static examples = [ @@ -74,118 +59,74 @@ export class ImportWorkflowsCommand extends Command { }), }; - ownerWorkflowRole: Role; + private ownerWorkflowRole: Role; - transactionManager: EntityManager; + private transactionManager: EntityManager; - async run(): Promise { - const logger = getLogger(); - LoggerProxy.init(logger); + async init() { + disableAutoGeneratedIds(WorkflowEntity); + await super.init(); + } + async run(): Promise { + // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(ImportWorkflowsCommand); if (!flags.input) { - console.info('An input file or directory with --input must be provided'); + this.logger.info('An input file or directory with --input must be provided'); return; } if (flags.separate) { if (fs.existsSync(flags.input)) { if (!fs.lstatSync(flags.input).isDirectory()) { - console.info('The argument to --input must be a directory'); + this.logger.info('The argument to --input must be a directory'); return; } } } - try { - disableAutoGeneratedIds(WorkflowEntity); - - await Db.init(); - - await this.initOwnerWorkflowRole(); - const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner(); - - // Make sure the settings exist - await UserSettings.prepareUserSettings(); - const credentials = await Db.collections.Credentials.find(); - const tags = await Db.collections.Tag.find(); - - let totalImported = 0; - - if (flags.separate) { - let { input: inputPath } = flags; - - if (process.platform === 'win32') { - inputPath = inputPath.replace(/\\/g, '/'); - } - - const files = await glob('*.json', { - cwd: inputPath, - absolute: true, - }); + await this.initOwnerWorkflowRole(); + const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner(); - totalImported = files.length; - console.info(`Importing ${totalImported} workflows...`); - await Db.getConnection().transaction(async (transactionManager) => { - this.transactionManager = transactionManager; + const credentials = await Db.collections.Credentials.find(); + const tags = await Db.collections.Tag.find(); - for (const file of files) { - const workflow = JSON.parse(fs.readFileSync(file, { encoding: 'utf8' })); + let totalImported = 0; - if (credentials.length > 0) { - workflow.nodes.forEach((node: INode) => { - this.transformCredentials(node, credentials); - - if (!node.id) { - // eslint-disable-next-line no-param-reassign - node.id = uuid(); - } - }); - } - - if (Object.prototype.hasOwnProperty.call(workflow, 'tags')) { - await setTagsForImport(transactionManager, workflow, tags); - } - - await this.storeWorkflow(workflow, user); - } - }); + if (flags.separate) { + let { input: inputPath } = flags; - this.reportSuccess(totalImported); - process.exit(); + if (process.platform === 'win32') { + inputPath = inputPath.replace(/\\/g, '/'); } - const workflows = JSON.parse(fs.readFileSync(flags.input, { encoding: 'utf8' })); - - assertHasWorkflowsToImport(workflows); - - totalImported = workflows.length; + const files = await glob('*.json', { + cwd: inputPath, + absolute: true, + }); + totalImported = files.length; + this.logger.info(`Importing ${totalImported} workflows...`); await Db.getConnection().transaction(async (transactionManager) => { this.transactionManager = transactionManager; - for (const workflow of workflows) { - let oldCredentialFormat = false; + for (const file of files) { + const workflow = jsonParse( + fs.readFileSync(file, { encoding: 'utf8' }), + ); + if (credentials.length > 0) { workflow.nodes.forEach((node: INode) => { this.transformCredentials(node, credentials); + if (!node.id) { // eslint-disable-next-line no-param-reassign node.id = uuid(); } - if (!node.credentials?.id) { - oldCredentialFormat = true; - } }); } - if (oldCredentialFormat) { - try { - await replaceInvalidCredentials(workflow as unknown as WorkflowEntity); - } catch (error) { - console.log(error); - } - } + if (Object.prototype.hasOwnProperty.call(workflow, 'tags')) { await setTagsForImport(transactionManager, workflow, tags); } @@ -196,15 +137,58 @@ export class ImportWorkflowsCommand extends Command { this.reportSuccess(totalImported); process.exit(); - } catch (error) { - console.error('An error occurred while importing workflows. See log messages for details.'); - if (error instanceof Error) logger.error(error.message); - this.exit(1); } + + const workflows = jsonParse( + fs.readFileSync(flags.input, { encoding: 'utf8' }), + ); + + assertHasWorkflowsToImport(workflows); + + totalImported = workflows.length; + + await Db.getConnection().transaction(async (transactionManager) => { + this.transactionManager = transactionManager; + + for (const workflow of workflows) { + let oldCredentialFormat = false; + if (credentials.length > 0) { + workflow.nodes.forEach((node: INode) => { + this.transformCredentials(node, credentials); + if (!node.id) { + // eslint-disable-next-line no-param-reassign + node.id = uuid(); + } + if (!node.credentials?.id) { + oldCredentialFormat = true; + } + }); + } + if (oldCredentialFormat) { + try { + await replaceInvalidCredentials(workflow as unknown as WorkflowEntity); + } catch (error) { + this.logger.error('Failed to replace invalid credential', error as Error); + } + } + if (Object.prototype.hasOwnProperty.call(workflow, 'tags')) { + await setTagsForImport(transactionManager, workflow, tags); + } + + await this.storeWorkflow(workflow, user); + } + }); + + this.reportSuccess(totalImported); + } + + async catch(error: Error) { + this.logger.error('An error occurred while importing workflows. See log messages for details.'); + this.logger.error(error.message); } private reportSuccess(total: number) { - console.info(`Successfully imported ${total} ${total === 1 ? 'workflow.' : 'workflows.'}`); + this.logger.info(`Successfully imported ${total} ${total === 1 ? 'workflow.' : 'workflows.'}`); } private async initOwnerWorkflowRole() { @@ -213,7 +197,7 @@ export class ImportWorkflowsCommand extends Command { }); if (!ownerWorkflowRole) { - throw new Error(`Failed to find owner workflow role. ${FIX_INSTRUCTION}`); + throw new Error(`Failed to find owner workflow role. ${UM_FIX_INSTRUCTION}`); } this.ownerWorkflowRole = ownerWorkflowRole; @@ -224,7 +208,7 @@ export class ImportWorkflowsCommand extends Command { await this.transactionManager.upsert( SharedWorkflow, { - workflowId: result.identifiers[0].id, + workflowId: result.identifiers[0].id as string, userId: user.id, roleId: this.ownerWorkflowRole.id, }, @@ -247,7 +231,7 @@ export class ImportWorkflowsCommand extends Command { (await Db.collections.User.findOneBy({ globalRoleId: ownerGlobalRole?.id })); if (!owner) { - throw new Error(`Failed to find owner. ${FIX_INSTRUCTION}`); + throw new Error(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); } return owner; diff --git a/packages/cli/src/commands/ldap/reset.ts b/packages/cli/src/commands/ldap/reset.ts index 61e420acb024d..7b555fc52ea8a 100644 --- a/packages/cli/src/commands/ldap/reset.ts +++ b/packages/cli/src/commands/ldap/reset.ts @@ -7,14 +7,16 @@ export class Reset extends BaseCommand { static description = '\nResets the database to the default ldap state'; async run(): Promise { - const ldapIdentities = await Db.collections.AuthIdentity.find({ + // eslint-disable-next-line @typescript-eslint/naming-convention + const { AuthIdentity, AuthProviderSyncHistory, Settings, User } = Db.collections; + const ldapIdentities = await AuthIdentity.find({ where: { providerType: 'ldap' }, select: ['userId'], }); - await Db.collections.AuthProviderSyncHistory.delete({ providerType: 'ldap' }); - await Db.collections.AuthIdentity.delete({ providerType: 'ldap' }); - await Db.collections.User.delete({ id: In(ldapIdentities.map((i) => i.userId)) }); - await Db.collections.Settings.delete({ key: LDAP_FEATURE_NAME }); + await AuthProviderSyncHistory.delete({ providerType: 'ldap' }); + await AuthIdentity.delete({ providerType: 'ldap' }); + await User.delete({ id: In(ldapIdentities.map((i) => i.userId)) }); + await Settings.delete({ key: LDAP_FEATURE_NAME }); this.logger.info('Successfully reset the database to default ldap state.'); } @@ -22,6 +24,5 @@ export class Reset extends BaseCommand { async catch(error: Error): Promise { this.logger.error('Error resetting database. See log messages for details.'); this.logger.error(error.message); - this.exit(1); } } diff --git a/packages/cli/src/commands/license/clear.ts b/packages/cli/src/commands/license/clear.ts index c2dc3d35faf31..d7e3bf51d34f0 100644 --- a/packages/cli/src/commands/license/clear.ts +++ b/packages/cli/src/commands/license/clear.ts @@ -1,42 +1,25 @@ -import { Command } from '@oclif/command'; - -import { LoggerProxy } from 'n8n-workflow'; - import * as Db from '@/Db'; - -import { getLogger } from '@/Logger'; import { SETTINGS_LICENSE_CERT_KEY } from '@/constants'; +import { BaseCommand } from '../BaseCommand'; -export class ClearLicenseCommand extends Command { +export class ClearLicenseCommand extends BaseCommand { static description = 'Clear license'; static examples = ['$ n8n clear:license']; async run() { - const logger = getLogger(); - LoggerProxy.init(logger); - - try { - await Db.init(); - - console.info('Clearing license from database.'); - await Db.collections.Settings.delete({ - key: SETTINGS_LICENSE_CERT_KEY, - }); - console.info('Done. Restart n8n to take effect.'); - } catch (e: unknown) { - console.error('Error updating database. See log messages for details.'); - logger.error('\nGOT ERROR'); - logger.info('===================================='); - if (e instanceof Error) { - logger.error(e.message); - if (e.stack) { - logger.error(e.stack); - } - } - this.exit(1); - } + this.logger.info('Clearing license from database.'); + await Db.collections.Settings.delete({ + key: SETTINGS_LICENSE_CERT_KEY, + }); + this.logger.info('Done. Restart n8n to take effect.'); + } - this.exit(); + async catch(error: Error) { + this.logger.error('Error updating database. See log messages for details.'); + this.logger.error('\nGOT ERROR'); + this.logger.info('===================================='); + this.logger.error(error.message); + this.logger.error(error.stack!); } } diff --git a/packages/cli/src/commands/list/workflow.ts b/packages/cli/src/commands/list/workflow.ts index 95ab06dfb8bb1..240e56d4fdedd 100644 --- a/packages/cli/src/commands/list/workflow.ts +++ b/packages/cli/src/commands/list/workflow.ts @@ -1,12 +1,9 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable no-console */ -import { Command, flags } from '@oclif/command'; - +import { flags } from '@oclif/command'; import type { IDataObject } from 'n8n-workflow'; - import * as Db from '@/Db'; +import { BaseCommand } from '../BaseCommand'; -export class ListWorkflowCommand extends Command { +export class ListWorkflowCommand extends BaseCommand { static description = '\nList workflows'; static examples = [ @@ -25,7 +22,6 @@ export class ListWorkflowCommand extends Command { }), }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async run() { // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(ListWorkflowCommand); @@ -34,28 +30,23 @@ export class ListWorkflowCommand extends Command { 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.forEach((workflow) => console.log(workflow.id)); - } else { - workflows.forEach((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); + const findQuery: IDataObject = {}; + if (flags.active !== undefined) { + findQuery.active = flags.active === 'true'; } - this.exit(); + const workflows = await Db.collections.Workflow.find(findQuery); + if (flags.onlyId) { + workflows.forEach((workflow) => this.logger.info(workflow.id)); + } else { + workflows.forEach((workflow) => this.logger.info(`${workflow.id}|${workflow.name}`)); + } + } + + async catch(error: Error) { + this.logger.error('\nGOT ERROR'); + this.logger.error('===================================='); + this.logger.error(error.message); + this.logger.error(error.stack!); } } diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 90b495fe791b4..28b092783037f 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -6,39 +6,31 @@ import path from 'path'; import { mkdir } from 'fs/promises'; import { createReadStream, createWriteStream, existsSync } from 'fs'; import localtunnel from 'localtunnel'; -import { BinaryDataManager, TUNNEL_SUBDOMAIN_ENV, UserSettings } from 'n8n-core'; -import { Command, flags } from '@oclif/command'; +import { TUNNEL_SUBDOMAIN_ENV, UserSettings } from 'n8n-core'; +import { flags } from '@oclif/command'; import stream from 'stream'; import replaceStream from 'replacestream'; import { promisify } from 'util'; import glob from 'fast-glob'; -import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow'; +import { LoggerProxy, sleep, jsonParse } from 'n8n-workflow'; import { createHash } from 'crypto'; import config from '@/config'; import * as ActiveExecutions from '@/ActiveExecutions'; import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner'; -import { CredentialsOverwrites } from '@/CredentialsOverwrites'; -import { CredentialTypes } from '@/CredentialTypes'; import * as Db from '@/Db'; -import { ExternalHooks } from '@/ExternalHooks'; import * as GenericHelpers from '@/GenericHelpers'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; -import { NodeTypes } from '@/NodeTypes'; import { InternalHooksManager } from '@/InternalHooksManager'; import * as Server from '@/Server'; import * as TestWebhooks from '@/TestWebhooks'; import { WaitTracker } from '@/WaitTracker'; - -import { getLogger } from '@/Logger'; import { getAllInstalledPackages } from '@/CommunityNodes/packageModel'; import { handleLdapInit } from '@/Ldap/helpers'; -import { initErrorHandling } from '@/ErrorReporting'; -import * as CrashJournal from '@/CrashJournal'; import { createPostHogLoadingScript } from '@/telemetry/scripts'; import { EDITOR_UI_DIST_DIR, GENERATED_STATIC_DIR } from '@/constants'; -import { eventBus } from '../eventbus'; +import { eventBus } from '@/eventbus'; +import { BaseCommand } from './BaseCommand'; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires const open = require('open'); @@ -46,21 +38,7 @@ const pipeline = promisify(stream.pipeline); let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined; -const exitWithCrash = async (message: string, error: unknown) => { - ErrorReporter.error(new Error(message, { cause: error }), { level: 'fatal' }); - await sleep(2000); - process.exit(1); -}; - -const exitSuccessFully = async () => { - try { - await CrashJournal.cleanup(); - } finally { - process.exit(); - } -}; - -export class Start extends Command { +export class Start extends BaseCommand { static description = 'Starts n8n. Makes Web-UI available and starts active workflows'; static examples = [ @@ -89,8 +67,7 @@ export class Start extends Command { /** * Opens the UI in browser */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - static openBrowser() { + private openBrowser() { const editorUrl = GenericHelpers.getBaseUrl(); // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -106,22 +83,20 @@ export class Start extends Command { * Make for example sure that all the webhooks from third party services * get removed. */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - static async stopProcess() { - getLogger().info('\nStopping n8n...'); + async stopProcess() { + this.logger.info('\nStopping n8n...'); try { // Stop with trying to activate workflows that could not be activated activeWorkflowRunner?.removeAllQueuedWorkflowActivations(); - const externalHooks = ExternalHooks(); - await externalHooks.run('n8n.stop', []); + await this.externalHooks.run('n8n.stop', []); setTimeout(async () => { // In case that something goes wrong with shutdown we // kill after max. 30 seconds no matter what console.log('process exited after 30s'); - await exitSuccessFully(); + await this.exitSuccessFully(); }, 30000); await InternalHooksManager.getInstance().onN8nStop(); @@ -162,13 +137,13 @@ export class Start extends Command { //finally shut down Event Bus await eventBus.close(); } catch (error) { - await exitWithCrash('There was an error shutting down n8n.', error); + await this.exitWithCrash('There was an error shutting down n8n.', error); } - await exitSuccessFully(); + await this.exitSuccessFully(); } - static async generateStaticAssets() { + private async generateStaticAssets() { // Read the index file and replace the path placeholder const n8nPath = config.getEnv('path'); const hooksUrls = config.getEnv('externalFrontendHooksUrls'); @@ -223,239 +198,206 @@ export class Start extends Command { await Promise.all(files.map(compileFile)); } - async run() { + async init() { // Make sure that n8n shuts down gracefully if possible - process.once('SIGTERM', Start.stopProcess); - process.once('SIGINT', Start.stopProcess); + process.once('SIGTERM', this.stopProcess); + process.once('SIGINT', this.stopProcess); - const logger = getLogger(); - LoggerProxy.init(logger); - logger.info('Initializing n8n process'); + await this.initCrashJournal(); + await super.init(); + this.logger.info('Initializing n8n process'); - await initErrorHandling(); - await CrashJournal.init(); + await this.initBinaryManager(); + await this.initExternalHooks(); + + if (!config.getEnv('endpoints.disableUi')) { + await this.generateStaticAssets(); + } + } + async run() { // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(Start); - try { - // Load all node and credential types - const loadNodesAndCredentials = LoadNodesAndCredentials(); - await loadNodesAndCredentials.init(); - - // Add the found types to an instance other parts of the application can use - const nodeTypes = NodeTypes(loadNodesAndCredentials); - const credentialTypes = CredentialTypes(loadNodesAndCredentials); - - // Start directly with the init of the database to improve startup time - await Db.init().catch(async (error: Error) => - exitWithCrash('There was an error initializing DB', error), - ); - - // Make sure the settings exist - const userSettings = await UserSettings.prepareUserSettings(); + if (!config.getEnv('userManagement.jwtSecret')) { + // If we don't have a JWT secret set, generate + // one based and save to config. + const encryptionKey = await UserSettings.getEncryptionKey(); - if (!config.getEnv('userManagement.jwtSecret')) { - // If we don't have a JWT secret set, generate - // one based and save to config. - const encryptionKey = await UserSettings.getEncryptionKey(); - - // For a key off every other letter from encryption key - // CAREFUL: do not change this or it breaks all existing tokens. - let baseKey = ''; - for (let i = 0; i < encryptionKey.length; i += 2) { - baseKey += encryptionKey[i]; - } - config.set('userManagement.jwtSecret', createHash('sha256').update(baseKey).digest('hex')); - } - - if (!config.getEnv('endpoints.disableUi')) { - await Start.generateStaticAssets(); + // For a key off every other letter from encryption key + // CAREFUL: do not change this or it breaks all existing tokens. + let baseKey = ''; + for (let i = 0; i < encryptionKey.length; i += 2) { + baseKey += encryptionKey[i]; } + config.set('userManagement.jwtSecret', createHash('sha256').update(baseKey).digest('hex')); + } - // Load all external hooks - const externalHooks = ExternalHooks(); - await externalHooks.init(); - - // Load the credentials overwrites if any exist - CredentialsOverwrites(credentialTypes); - - await loadNodesAndCredentials.generateTypesForFrontend(); - - const installedPackages = await getAllInstalledPackages(); - const missingPackages = new Set<{ - packageName: string; - version: string; - }>(); - installedPackages.forEach((installedPackage) => { - installedPackage.installedNodes.forEach((installedNode) => { - if (!loadNodesAndCredentials.known.nodes[installedNode.type]) { - // Leave the list ready for installing in case we need. - missingPackages.add({ - packageName: installedPackage.packageName, - version: installedPackage.installedVersion, - }); - } - }); + await this.loadNodesAndCredentials.generateTypesForFrontend(); + + const installedPackages = await getAllInstalledPackages(); + const missingPackages = new Set<{ + packageName: string; + version: string; + }>(); + installedPackages.forEach((installedPackage) => { + installedPackage.installedNodes.forEach((installedNode) => { + if (!this.loadNodesAndCredentials.known.nodes[installedNode.type]) { + // Leave the list ready for installing in case we need. + missingPackages.add({ + packageName: installedPackage.packageName, + version: installedPackage.installedVersion, + }); + } }); + }); - await UserSettings.getEncryptionKey(); + await UserSettings.getEncryptionKey(); - // Load settings from database and set them to config. - const databaseSettings = await Db.collections.Settings.findBy({ loadOnStartup: true }); - databaseSettings.forEach((setting) => { - config.set(setting.key, JSON.parse(setting.value)); - }); + // Load settings from database and set them to config. + const databaseSettings = await Db.collections.Settings.findBy({ loadOnStartup: true }); + databaseSettings.forEach((setting) => { + config.set(setting.key, jsonParse(setting.value)); + }); + + config.set('nodes.packagesMissing', ''); + if (missingPackages.size) { + LoggerProxy.error( + 'n8n detected that some packages are missing. For more information, visit https://docs.n8n.io/integrations/community-nodes/troubleshooting/', + ); - config.set('nodes.packagesMissing', ''); - if (missingPackages.size) { - LoggerProxy.error( - 'n8n detected that some packages are missing. For more information, visit https://docs.n8n.io/integrations/community-nodes/troubleshooting/', - ); - - if (flags.reinstallMissingPackages || process.env.N8N_REINSTALL_MISSING_PACKAGES) { - LoggerProxy.info('Attempting to reinstall missing packages', { missingPackages }); - try { - // Optimistic approach - stop if any installation fails - // eslint-disable-next-line no-restricted-syntax - for (const missingPackage of missingPackages) { - // eslint-disable-next-line no-await-in-loop - void (await loadNodesAndCredentials.loadNpmModule( - missingPackage.packageName, - missingPackage.version, - )); - missingPackages.delete(missingPackage); - } - LoggerProxy.info('Packages reinstalled successfully. Resuming regular initialization.'); - } catch (error) { - LoggerProxy.error('n8n was unable to install the missing packages.'); + if (flags.reinstallMissingPackages || process.env.N8N_REINSTALL_MISSING_PACKAGES) { + LoggerProxy.info('Attempting to reinstall missing packages', { missingPackages }); + try { + // Optimistic approach - stop if any installation fails + // eslint-disable-next-line no-restricted-syntax + for (const missingPackage of missingPackages) { + // eslint-disable-next-line no-await-in-loop + void (await this.loadNodesAndCredentials.loadNpmModule( + missingPackage.packageName, + missingPackage.version, + )); + missingPackages.delete(missingPackage); } + LoggerProxy.info('Packages reinstalled successfully. Resuming regular initialization.'); + } catch (error) { + LoggerProxy.error('n8n was unable to install the missing packages.'); } - - config.set( - 'nodes.packagesMissing', - Array.from(missingPackages) - .map((missingPackage) => `${missingPackage.packageName}@${missingPackage.version}`) - .join(' '), - ); } - const dbType = config.getEnv('database.type'); - if (dbType === 'sqlite') { - const shouldRunVacuum = config.getEnv('database.sqlite.executeVacuumOnStartup'); - if (shouldRunVacuum) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - await Db.collections.Execution.query('VACUUM;'); - } + config.set( + 'nodes.packagesMissing', + Array.from(missingPackages) + .map((missingPackage) => `${missingPackage.packageName}@${missingPackage.version}`) + .join(' '), + ); + } + + const dbType = config.getEnv('database.type'); + if (dbType === 'sqlite') { + const shouldRunVacuum = config.getEnv('database.sqlite.executeVacuumOnStartup'); + if (shouldRunVacuum) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + await Db.collections.Execution.query('VACUUM;'); } + } - if (flags.tunnel) { - this.log('\nWaiting for tunnel ...'); - - let tunnelSubdomain; - if ( - process.env[TUNNEL_SUBDOMAIN_ENV] !== undefined && - process.env[TUNNEL_SUBDOMAIN_ENV] !== '' - ) { - tunnelSubdomain = process.env[TUNNEL_SUBDOMAIN_ENV]; - } else if (userSettings.tunnelSubdomain !== undefined) { - tunnelSubdomain = userSettings.tunnelSubdomain; - } + if (flags.tunnel) { + this.log('\nWaiting for tunnel ...'); + + let tunnelSubdomain; + if ( + process.env[TUNNEL_SUBDOMAIN_ENV] !== undefined && + process.env[TUNNEL_SUBDOMAIN_ENV] !== '' + ) { + tunnelSubdomain = process.env[TUNNEL_SUBDOMAIN_ENV]; + } else if (this.userSettings.tunnelSubdomain !== undefined) { + tunnelSubdomain = this.userSettings.tunnelSubdomain; + } - if (tunnelSubdomain === undefined) { - // When no tunnel subdomain did exist yet create a new random one - const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789'; - userSettings.tunnelSubdomain = Array.from({ length: 24 }) - .map(() => { - return availableCharacters.charAt( - Math.floor(Math.random() * availableCharacters.length), - ); - }) - .join(''); - - await UserSettings.writeUserSettings(userSettings); - } + if (tunnelSubdomain === undefined) { + // When no tunnel subdomain did exist yet create a new random one + const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789'; + this.userSettings.tunnelSubdomain = Array.from({ length: 24 }) + .map(() => { + return availableCharacters.charAt( + Math.floor(Math.random() * availableCharacters.length), + ); + }) + .join(''); + + await UserSettings.writeUserSettings(this.userSettings); + } - const tunnelSettings: localtunnel.TunnelConfig = { - host: 'https://hooks.n8n.cloud', - subdomain: tunnelSubdomain, - }; + const tunnelSettings: localtunnel.TunnelConfig = { + host: 'https://hooks.n8n.cloud', + subdomain: tunnelSubdomain, + }; - const port = config.getEnv('port'); + const port = config.getEnv('port'); - // @ts-ignore - const webhookTunnel = await localtunnel(port, tunnelSettings); + // @ts-ignore + const webhookTunnel = await localtunnel(port, tunnelSettings); - process.env.WEBHOOK_URL = `${webhookTunnel.url}/`; - this.log(`Tunnel URL: ${process.env.WEBHOOK_URL}\n`); - this.log( - 'IMPORTANT! Do not share with anybody as it would give people access to your n8n instance!', - ); - } + process.env.WEBHOOK_URL = `${webhookTunnel.url}/`; + this.log(`Tunnel URL: ${process.env.WEBHOOK_URL}\n`); + this.log( + 'IMPORTANT! Do not share with anybody as it would give people access to your n8n instance!', + ); + } - const instanceId = await UserSettings.getInstanceId(); - await InternalHooksManager.init(instanceId, nodeTypes); + await Server.start(); - const binaryDataConfig = config.getEnv('binaryDataManager'); - await BinaryDataManager.init(binaryDataConfig, true); + // Start to get active workflows and run their triggers + activeWorkflowRunner = ActiveWorkflowRunner.getInstance(); + await activeWorkflowRunner.init(); - await Server.start(); + WaitTracker(); - // Start to get active workflows and run their triggers - activeWorkflowRunner = ActiveWorkflowRunner.getInstance(); - await activeWorkflowRunner.init(); + await handleLdapInit(); - WaitTracker(); + const editorUrl = GenericHelpers.getBaseUrl(); + this.log(`\nEditor is now accessible via:\n${editorUrl}`); - await handleLdapInit(); + const saveManualExecutions = config.getEnv('executions.saveDataManualExecutions'); - const editorUrl = GenericHelpers.getBaseUrl(); - this.log(`\nEditor is now accessible via:\n${editorUrl}`); + if (saveManualExecutions) { + this.log('\nManual executions will be visible only for the owner'); + } - const saveManualExecutions = config.getEnv('executions.saveDataManualExecutions'); + // Allow to open n8n editor by pressing "o" + if (Boolean(process.stdout.isTTY) && process.stdin.setRawMode) { + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.setEncoding('utf8'); - if (saveManualExecutions) { - this.log('\nManual executions will be visible only for the owner'); + if (flags.open) { + this.openBrowser(); } - - // Allow to open n8n editor by pressing "o" - if (Boolean(process.stdout.isTTY) && process.stdin.setRawMode) { - process.stdin.setRawMode(true); - process.stdin.resume(); - process.stdin.setEncoding('utf8'); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let inputText = ''; - - if (flags.open) { - Start.openBrowser(); - } - this.log('\nPress "o" to open in Browser.'); - process.stdin.on('data', (key: string) => { - if (key === 'o') { - Start.openBrowser(); - inputText = ''; - } else if (key.charCodeAt(0) === 3) { - // Ctrl + c got pressed - // eslint-disable-next-line @typescript-eslint/no-floating-promises - Start.stopProcess(); + this.log('\nPress "o" to open in Browser.'); + process.stdin.on('data', (key: string) => { + if (key === 'o') { + this.openBrowser(); + } else if (key.charCodeAt(0) === 3) { + // Ctrl + c got pressed + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.stopProcess(); + } else { + // When anything else got pressed, record it and send it on enter into the child process + // eslint-disable-next-line no-lonely-if + if (key.charCodeAt(0) === 13) { + // send to child process and print in terminal + process.stdout.write('\n'); } else { - // When anything else got pressed, record it and send it on enter into the child process - // eslint-disable-next-line no-lonely-if - if (key.charCodeAt(0) === 13) { - // send to child process and print in terminal - process.stdout.write('\n'); - inputText = ''; - } else { - // record it and write into terminal - // eslint-disable-next-line @typescript-eslint/no-unused-vars - inputText += key; - process.stdout.write(key); - } + // record it and write into terminal + process.stdout.write(key); } - }); - } - } catch (error) { - await exitWithCrash('There was an error', error); + } + }); } } + + async catch(error: Error) { + await this.exitWithCrash('Exiting due to an error.', error); + } } diff --git a/packages/cli/src/commands/update/workflow.ts b/packages/cli/src/commands/update/workflow.ts index 94ef751e75232..a26fd80516560 100644 --- a/packages/cli/src/commands/update/workflow.ts +++ b/packages/cli/src/commands/update/workflow.ts @@ -1,16 +1,12 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable no-console */ -import { Command, flags } from '@oclif/command'; - +import { flags } from '@oclif/command'; import type { IDataObject } from 'n8n-workflow'; -import { LoggerProxy } from 'n8n-workflow'; - import * as Db from '@/Db'; +import { BaseCommand } from '../BaseCommand'; -import { getLogger } from '@/Logger'; - -export class UpdateWorkflowCommand extends Command { +export class UpdateWorkflowCommand extends BaseCommand { static description = 'Update workflows'; static examples = [ @@ -31,11 +27,7 @@ export class UpdateWorkflowCommand extends Command { }), }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async run() { - const logger = getLogger(); - LoggerProxy.init(logger); - // eslint-disable-next-line @typescript-eslint/no-shadow const { flags } = this.parse(UpdateWorkflowCommand); @@ -56,35 +48,32 @@ export class UpdateWorkflowCommand extends Command { console.info('No update flag like "--active=true" has been set!'); return; } + if (!['false', 'true'].includes(flags.active)) { console.info('Valid values for flag "--active" are only "false" or "true"!'); return; } - updateQuery.active = flags.active === 'true'; - - try { - await Db.init(); - const findQuery: IDataObject = {}; - if (flags.id) { - console.info(`Deactivating workflow with ID: ${flags.id}`); - findQuery.id = flags.id; - } else { - console.info('Deactivating all workflows'); - findQuery.active = true; - } + updateQuery.active = flags.active === 'true'; - await Db.collections.Workflow.update(findQuery, updateQuery); - console.info('Done'); - } catch (e) { - console.error('Error updating database. See log messages for details.'); - logger.error('\nGOT ERROR'); - logger.info('===================================='); - logger.error(e.message); - logger.error(e.stack); - this.exit(1); + const findQuery: IDataObject = {}; + if (flags.id) { + this.logger.info(`Deactivating workflow with ID: ${flags.id}`); + findQuery.id = flags.id; + } else { + this.logger.info('Deactivating all workflows'); + findQuery.active = true; } - this.exit(); + await Db.collections.Workflow.update(findQuery, updateQuery); + this.logger.info('Done'); + } + + async catch(error: Error) { + this.logger.error('Error updating database. See log messages for details.'); + this.logger.error('\nGOT ERROR'); + this.logger.error('===================================='); + this.logger.error(error.message); + this.logger.error(error.stack!); } } diff --git a/packages/cli/src/commands/user-management/reset.ts b/packages/cli/src/commands/user-management/reset.ts index 36179b42dcaa6..8c3bac1a96dc6 100644 --- a/packages/cli/src/commands/user-management/reset.ts +++ b/packages/cli/src/commands/user-management/reset.ts @@ -1,10 +1,21 @@ import { Not } from 'typeorm'; import * as Db from '@/Db'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; +import { User } from '@db/entities/User'; import { BaseCommand } from '../BaseCommand'; +const defaultUserProps = { + firstName: null, + lastName: null, + email: null, + password: null, + resetPasswordToken: null, +}; + export class Reset extends BaseCommand { - static description = '\nResets the database to the default user state'; + static description = 'Resets the database to the default user state'; + + static examples = ['$ n8n user-management:reset']; async run(): Promise { const owner = await this.getInstanceOwner(); @@ -30,7 +41,7 @@ export class Reset extends BaseCommand { ); await Db.collections.User.delete({ id: Not(owner.id) }); - await Db.collections.User.save(Object.assign(owner, this.defaultUserProps)); + await Db.collections.User.save(Object.assign(owner, defaultUserProps)); const danglingCredentials: CredentialsEntity[] = (await Db.collections.Credentials.createQueryBuilder('credentials') @@ -58,6 +69,25 @@ export class Reset extends BaseCommand { this.logger.info('Successfully reset the database to default user state.'); } + async getInstanceOwner(): Promise { + const globalRole = await Db.collections.Role.findOneByOrFail({ + name: 'owner', + scope: 'global', + }); + + const owner = await Db.collections.User.findOneBy({ globalRoleId: globalRole.id }); + + if (owner) return owner; + + const user = new User(); + + Object.assign(user, { ...defaultUserProps, globalRole }); + + await Db.collections.User.save(user); + + return Db.collections.User.findOneByOrFail({ globalRoleId: globalRole.id }); + } + async catch(error: Error): Promise { this.logger.error('Error resetting database. See log messages for details.'); this.logger.error(error.message); diff --git a/packages/cli/src/commands/webhook.ts b/packages/cli/src/commands/webhook.ts index ed47095c66f64..e8cafdbfbc13f 100644 --- a/packages/cli/src/commands/webhook.ts +++ b/packages/cli/src/commands/webhook.ts @@ -1,37 +1,12 @@ /* eslint-disable @typescript-eslint/unbound-method */ -import { BinaryDataManager, UserSettings } from 'n8n-core'; -import { Command, flags } from '@oclif/command'; - -import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow'; +import { flags } from '@oclif/command'; +import { LoggerProxy, sleep } from 'n8n-workflow'; import config from '@/config'; import * as ActiveExecutions from '@/ActiveExecutions'; -import { CredentialsOverwrites } from '@/CredentialsOverwrites'; -import { CredentialTypes } from '@/CredentialTypes'; -import * as Db from '@/Db'; -import { ExternalHooks } from '@/ExternalHooks'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; -import { NodeTypes } from '@/NodeTypes'; -import { InternalHooksManager } from '@/InternalHooksManager'; import { WebhookServer } from '@/WebhookServer'; -import { getLogger } from '@/Logger'; -import { initErrorHandling } from '@/ErrorReporting'; -import * as CrashJournal from '@/CrashJournal'; - -const exitWithCrash = async (message: string, error: unknown) => { - ErrorReporter.error(new Error(message, { cause: error }), { level: 'fatal' }); - await sleep(2000); - process.exit(1); -}; - -const exitSuccessFully = async () => { - try { - await CrashJournal.cleanup(); - } finally { - process.exit(); - } -}; +import { BaseCommand } from './BaseCommand'; -export class Webhook extends Command { +export class Webhook extends BaseCommand { static description = 'Starts n8n webhook process. Intercepts only production URLs.'; static examples = ['$ n8n webhook']; @@ -45,18 +20,16 @@ export class Webhook extends Command { * Make for example sure that all the webhooks from third party services * get removed. */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - static async stopProcess() { + async stopProcess() { LoggerProxy.info('\nStopping n8n...'); try { - const externalHooks = ExternalHooks(); - await externalHooks.run('n8n.stop', []); + await this.externalHooks.run('n8n.stop', []); setTimeout(async () => { // In case that something goes wrong with shutdown we // kill after max. 30 seconds no matter what - await exitSuccessFully(); + await this.exitSuccessFully(); }, 30000); // Wait for active workflow executions to finish @@ -75,14 +48,13 @@ export class Webhook extends Command { executingWorkflows = activeExecutionsInstance.getActiveExecutions(); } } catch (error) { - await exitWithCrash('There was an error shutting down n8n.', error); + await this.exitWithCrash('There was an error shutting down n8n.', error); } - await exitSuccessFully(); + await this.exitSuccessFully(); } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async run() { + async init() { if (config.getEnv('executions.mode') !== 'queue') { /** * It is technically possible to run without queues but @@ -99,56 +71,23 @@ export class Webhook extends Command { this.error('Webhook processes can only run with execution mode as queue.'); } - const logger = getLogger(); - LoggerProxy.init(logger); - // Make sure that n8n shuts down gracefully if possible - process.once('SIGTERM', Webhook.stopProcess); - process.once('SIGINT', Webhook.stopProcess); - - await initErrorHandling(); - await CrashJournal.init(); - - try { - // Start directly with the init of the database to improve startup time - const startDbInitPromise = Db.init().catch(async (error: Error) => - exitWithCrash('There was an error initializing DB', error), - ); - - // Make sure the settings exist - // eslint-disable-next-line @typescript-eslint/no-unused-vars - await UserSettings.prepareUserSettings(); - - // Load all node and credential types - const loadNodesAndCredentials = LoadNodesAndCredentials(); - await loadNodesAndCredentials.init(); + process.once('SIGTERM', this.stopProcess); + process.once('SIGINT', this.stopProcess); - // Add the found types to an instance other parts of the application can use - const nodeTypes = NodeTypes(loadNodesAndCredentials); - const credentialTypes = CredentialTypes(loadNodesAndCredentials); + await this.initCrashJournal(); + await super.init(); - // Load the credentials overwrites if any exist - CredentialsOverwrites(credentialTypes); - - // Load all external hooks - const externalHooks = ExternalHooks(); - await externalHooks.init(); - - // Wait till the database is ready - await startDbInitPromise; - - const instanceId = await UserSettings.getInstanceId(); - await InternalHooksManager.init(instanceId, nodeTypes); - - const binaryDataConfig = config.getEnv('binaryDataManager'); - await BinaryDataManager.init(binaryDataConfig); + await this.initBinaryManager(); + await this.initExternalHooks(); + } - const server = new WebhookServer(); - await server.start(); + async run() { + await new WebhookServer().start(); + this.logger.info('Webhook listener waiting for requests.'); + } - console.info('Webhook listener waiting for requests.'); - } catch (error) { - await exitWithCrash('Exiting due to error.', error); - } + async catch(error: Error) { + await this.exitWithCrash('Exiting due to an error.', error); } } diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index 895ed3988ad9d..591aa4f836c1e 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -1,54 +1,28 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-shadow */ /* eslint-disable @typescript-eslint/unbound-method */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ import express from 'express'; import http from 'http'; import type PCancelable from 'p-cancelable'; -import { Command, flags } from '@oclif/command'; -import { BinaryDataManager, UserSettings, WorkflowExecute } from 'n8n-core'; +import { flags } from '@oclif/command'; +import { WorkflowExecute } from 'n8n-core'; import type { IExecuteResponsePromiseData, INodeTypes, IRun } from 'n8n-workflow'; -import { Workflow, LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow'; +import { Workflow, NodeOperationError, LoggerProxy, sleep } from 'n8n-workflow'; -import { CredentialsOverwrites } from '@/CredentialsOverwrites'; -import { CredentialTypes } from '@/CredentialTypes'; import * as Db from '@/Db'; -import { ExternalHooks } from '@/ExternalHooks'; -import { NodeTypes } from '@/NodeTypes'; import * as ResponseHelper from '@/ResponseHelper'; import * as WebhookHelpers from '@/WebhookHelpers'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; -import { InternalHooksManager } from '@/InternalHooksManager'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; -import { getLogger } from '@/Logger'; import { PermissionChecker } from '@/UserManagement/PermissionChecker'; import config from '@/config'; import * as Queue from '@/Queue'; -import * as CrashJournal from '@/CrashJournal'; import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper'; import { generateFailedExecutionFromError } from '@/WorkflowHelpers'; import { N8N_VERSION } from '@/constants'; -import { initErrorHandling } from '@/ErrorReporting'; - -const exitWithCrash = async (message: string, error: unknown) => { - ErrorReporter.error(new Error(message, { cause: error }), { level: 'fatal' }); - await sleep(2000); - process.exit(1); -}; - -const exitSuccessFully = async () => { - try { - await CrashJournal.cleanup(); - } finally { - process.exit(); - } -}; +import { BaseCommand } from './BaseCommand'; -export class Worker extends Command { +export class Worker extends BaseCommand { static description = '\nStarts a n8n worker'; static examples = ['$ n8n worker --concurrency=5']; @@ -72,7 +46,7 @@ export class Worker extends Command { * Make for example sure that all the webhooks from third party services * get removed. */ - static async stopProcess() { + async stopProcess() { LoggerProxy.info('Stopping n8n...'); // Stop accepting new jobs @@ -80,8 +54,7 @@ export class Worker extends Command { Worker.jobQueue.pause(true); try { - const externalHooks = ExternalHooks(); - await externalHooks.run('n8n.stop', []); + await this.externalHooks.run('n8n.stop', []); const maxStopTime = config.getEnv('queue.bull.gracefulShutdownTimeout') * 1000; @@ -90,7 +63,7 @@ export class Worker extends Command { setTimeout(async () => { // In case that something goes wrong with shutdown we // kill after max. 30 seconds no matter what - await exitSuccessFully(); + await this.exitSuccessFully(); }, maxStopTime); // Wait for active workflow executions to finish @@ -108,10 +81,10 @@ export class Worker extends Command { await sleep(500); } } catch (error) { - await exitWithCrash('There was an error shutting down n8n.', error); + await this.exitWithCrash('There was an error shutting down n8n.', error); } - await exitSuccessFully(); + await this.exitSuccessFully(); } async runJob(job: Queue.Job, nodeTypes: INodeTypes): Promise { @@ -128,31 +101,27 @@ export class Worker extends Command { ); } const currentExecutionDb = ResponseHelper.unflattenExecutionData(executionDb); + const workflowId = currentExecutionDb.workflowData.id!; LoggerProxy.info( - `Start job: ${job.id} (Workflow ID: ${currentExecutionDb.workflowData.id} | Execution: ${executionId})`, + `Start job: ${job.id} (Workflow ID: ${workflowId} | Execution: ${executionId})`, ); - const workflowOwner = await getWorkflowOwner(currentExecutionDb.workflowData.id!.toString()); + const workflowOwner = await getWorkflowOwner(workflowId); let { staticData } = currentExecutionDb.workflowData; if (loadStaticData) { const workflowData = await Db.collections.Workflow.findOne({ select: ['id', 'staticData'], where: { - id: currentExecutionDb.workflowData.id, + id: workflowId, }, }); if (workflowData === null) { LoggerProxy.error( 'Worker execution failed because workflow could not be found in database.', - { - workflowId: currentExecutionDb.workflowData.id, - executionId, - }, - ); - throw new Error( - `The workflow with the ID "${currentExecutionDb.workflowData.id}" could not be found`, + { workflowId, executionId }, ); + throw new Error(`The workflow with the ID "${workflowId}" could not be found`); } staticData = workflowData.staticData; } @@ -173,7 +142,7 @@ export class Worker extends Command { } const workflow = new Workflow({ - id: currentExecutionDb.workflowData.id as string, + id: workflowId, name: currentExecutionDb.workflowData.name, nodes: currentExecutionDb.workflowData.nodes, connections: currentExecutionDb.workflowData.connections, @@ -198,15 +167,15 @@ export class Worker extends Command { try { await PermissionChecker.check(workflow, workflowOwner.id); } catch (error) { - const failedExecution = generateFailedExecutionFromError( - currentExecutionDb.mode, - error, - error.node, - ); - await additionalData.hooks.executeHookFunctions('workflowExecuteAfter', [failedExecution]); - return { - success: true, - }; + if (error instanceof NodeOperationError) { + const failedExecution = generateFailedExecutionFromError( + currentExecutionDb.mode, + error, + error.node, + ); + await additionalData.hooks.executeHookFunctions('workflowExecuteAfter', [failedExecution]); + } + return { success: true }; } additionalData.hooks.hookFunctions.sendResponse = [ @@ -249,184 +218,149 @@ export class Worker extends Command { }; } - async run() { - const logger = getLogger(); - LoggerProxy.init(logger); - - // eslint-disable-next-line no-console - console.info('Starting n8n worker...'); - + async init() { // Make sure that n8n shuts down gracefully if possible - process.once('SIGTERM', Worker.stopProcess); - process.once('SIGINT', Worker.stopProcess); - - await initErrorHandling(); - await CrashJournal.init(); + process.once('SIGTERM', this.stopProcess); + process.once('SIGINT', this.stopProcess); - // Wrap that the process does not close but we can still use async - await (async () => { - try { - const { flags } = this.parse(Worker); + await this.initCrashJournal(); + await super.init(); + this.logger.debug('Starting n8n worker...'); - // Start directly with the init of the database to improve startup time - const startDbInitPromise = Db.init().catch(async (error: Error) => - exitWithCrash('There was an error initializing DB', error), - ); + await this.initBinaryManager(); + await this.initExternalHooks(); + } - // Make sure the settings exist - await UserSettings.prepareUserSettings(); + async run() { + // eslint-disable-next-line @typescript-eslint/no-shadow + const { flags } = this.parse(Worker); - // Load all node and credential types - const loadNodesAndCredentials = LoadNodesAndCredentials(); - await loadNodesAndCredentials.init(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const redisConnectionTimeoutLimit = config.getEnv('queue.bull.redis.timeoutThreshold'); - // Add the found types to an instance other parts of the application can use - const nodeTypes = NodeTypes(loadNodesAndCredentials); - const credentialTypes = CredentialTypes(loadNodesAndCredentials); + const queue = await Queue.getInstance(); + Worker.jobQueue = queue.getBullObjectInstance(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, this.nodeTypes)); + + this.logger.info('\nn8n worker is now ready'); + this.logger.info(` * Version: ${N8N_VERSION}`); + this.logger.info(` * Concurrency: ${flags.concurrency}`); + this.logger.info(''); + + Worker.jobQueue.on('global:progress', (jobId: Queue.JobId, progress) => { + // Progress of a job got updated which does get used + // to communicate that a job got canceled. + + if (progress === -1) { + // Job has to get canceled + if (Worker.runningJobs[jobId] !== undefined) { + // Job is processed by current worker so cancel + Worker.runningJobs[jobId].cancel(); + delete Worker.runningJobs[jobId]; + } + } + }); - // Load the credentials overwrites if any exist - CredentialsOverwrites(credentialTypes); + let lastTimer = 0; + let cumulativeTimeout = 0; + Worker.jobQueue.on('error', (error: Error) => { + if (error.toString().includes('ECONNREFUSED')) { + const now = Date.now(); + if (now - lastTimer > 30000) { + // Means we had no timeout at all or last timeout was temporary and we recovered + lastTimer = now; + cumulativeTimeout = 0; + } else { + cumulativeTimeout += now - lastTimer; + lastTimer = now; + if (cumulativeTimeout > redisConnectionTimeoutLimit) { + this.logger.error( + `Unable to connect to Redis after ${redisConnectionTimeoutLimit}. Exiting process.`, + ); + process.exit(1); + } + } + this.logger.warn('Redis unavailable - trying to reconnect...'); + } else if (error.toString().includes('Error initializing Lua scripts')) { + // This is a non-recoverable error + // Happens when worker starts and Redis is unavailable + // Even if Redis comes back online, worker will be zombie + this.logger.error('Error initializing worker.'); + process.exit(2); + } else { + this.logger.error('Error from queue: ', error); + throw error; + } + }); - // Load all external hooks - const externalHooks = ExternalHooks(); - await externalHooks.init(); + if (config.getEnv('queue.health.active')) { + const port = config.getEnv('queue.health.port'); - // Wait till the database is ready - await startDbInitPromise; + const app = express(); + app.disable('x-powered-by'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const redisConnectionTimeoutLimit = config.getEnv('queue.bull.redis.timeoutThreshold'); + const server = http.createServer(app); - const queue = await Queue.getInstance(); - Worker.jobQueue = queue.getBullObjectInstance(); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, nodeTypes)); + app.get( + '/healthz', + // eslint-disable-next-line consistent-return + async (req: express.Request, res: express.Response) => { + LoggerProxy.debug('Health check started!'); - const instanceId = await UserSettings.getInstanceId(); + const connection = Db.getConnection(); - await InternalHooksManager.init(instanceId, nodeTypes); + try { + if (!connection.isInitialized) { + // Connection is not active + throw new Error('No active database connection!'); + } + // DB ping + await connection.query('SELECT 1'); + } catch (e) { + LoggerProxy.error('No Database connection!', e as Error); + const error = new ResponseHelper.ServiceUnavailableError('No Database connection!'); + return ResponseHelper.sendErrorResponse(res, error); + } - const binaryDataConfig = config.getEnv('binaryDataManager'); - await BinaryDataManager.init(binaryDataConfig); + // Just to be complete, generally will the worker stop automatically + // if it loses the connection to redis + try { + // Redis ping + await Worker.jobQueue.client.ping(); + } catch (e) { + LoggerProxy.error('No Redis connection!', e as Error); + const error = new ResponseHelper.ServiceUnavailableError('No Redis connection!'); + return ResponseHelper.sendErrorResponse(res, error); + } - console.info('\nn8n worker is now ready'); - console.info(` * Version: ${N8N_VERSION}`); - console.info(` * Concurrency: ${flags.concurrency}`); - console.info(''); + // Everything fine + const responseData = { + status: 'ok', + }; - Worker.jobQueue.on('global:progress', (jobId, progress) => { - // Progress of a job got updated which does get used - // to communicate that a job got canceled. + LoggerProxy.debug('Health check completed successfully!'); - if (progress === -1) { - // Job has to get canceled - if (Worker.runningJobs[jobId] !== undefined) { - // Job is processed by current worker so cancel - Worker.runningJobs[jobId].cancel(); - delete Worker.runningJobs[jobId]; - } - } - }); - - let lastTimer = 0; - let cumulativeTimeout = 0; - Worker.jobQueue.on('error', (error: Error) => { - if (error.toString().includes('ECONNREFUSED')) { - const now = Date.now(); - if (now - lastTimer > 30000) { - // Means we had no timeout at all or last timeout was temporary and we recovered - lastTimer = now; - cumulativeTimeout = 0; - } else { - cumulativeTimeout += now - lastTimer; - lastTimer = now; - if (cumulativeTimeout > redisConnectionTimeoutLimit) { - logger.error( - `Unable to connect to Redis after ${redisConnectionTimeoutLimit}. Exiting process.`, - ); - process.exit(1); - } - } - logger.warn('Redis unavailable - trying to reconnect...'); - } else if (error.toString().includes('Error initializing Lua scripts')) { - // This is a non-recoverable error - // Happens when worker starts and Redis is unavailable - // Even if Redis comes back online, worker will be zombie - logger.error('Error initializing worker.'); - process.exit(2); - } else { - logger.error('Error from queue: ', error); - throw error; - } - }); - - if (config.getEnv('queue.health.active')) { - const port = config.getEnv('queue.health.port'); - - const app = express(); - app.disable('x-powered-by'); - - const server = http.createServer(app); - - app.get( - '/healthz', - // eslint-disable-next-line consistent-return - async (req: express.Request, res: express.Response) => { - LoggerProxy.debug('Health check started!'); - - const connection = Db.getConnection(); - - try { - if (!connection.isInitialized) { - // Connection is not active - throw new Error('No active database connection!'); - } - // DB ping - await connection.query('SELECT 1'); - } catch (e) { - LoggerProxy.error('No Database connection!', e); - const error = new ResponseHelper.ServiceUnavailableError('No Database connection!'); - return ResponseHelper.sendErrorResponse(res, error); - } - - // Just to be complete, generally will the worker stop automatically - // if it loses the connection to redis - try { - // Redis ping - await Worker.jobQueue.client.ping(); - } catch (e) { - LoggerProxy.error('No Redis connection!', e); - const error = new ResponseHelper.ServiceUnavailableError('No Redis connection!'); - return ResponseHelper.sendErrorResponse(res, error); - } - - // Everything fine - const responseData = { - status: 'ok', - }; - - LoggerProxy.debug('Health check completed successfully!'); - - ResponseHelper.sendSuccessResponse(res, responseData, true, 200); - }, - ); + ResponseHelper.sendSuccessResponse(res, responseData, true, 200); + }, + ); - server.listen(port, () => { - console.info(`\nn8n worker health check via, port ${port}`); - }); + server.listen(port, () => { + this.logger.info(`\nn8n worker health check via, port ${port}`); + }); - server.on('error', (error: Error & { code: string }) => { - if (error.code === 'EADDRINUSE') { - console.log( - `n8n's port ${port} is already in use. Do you have the n8n main process running on that port?`, - ); - process.exit(1); - } - }); + server.on('error', (error: Error & { code: string }) => { + if (error.code === 'EADDRINUSE') { + this.logger.error( + `n8n's port ${port} is already in use. Do you have the n8n main process running on that port?`, + ); + process.exit(1); } - } catch (error) { - await exitWithCrash('Worker process cannot continue.', error); - } - })(); + }); + } + } + + async catch(error: Error) { + await this.exitWithCrash('Worker exiting due to an error.', error); } } diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index 9084784aa339e..5b2d2e08721a2 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -25,6 +25,8 @@ export function getN8nPackageJson() { return jsonParse(readFileSync(join(CLI_DIR, 'package.json'), 'utf8')); } +export const START_NODES = ['n8n-nodes-base.start', 'n8n-nodes-base.manualTrigger']; + export const N8N_VERSION = getN8nPackageJson().version; export const NODE_PACKAGE_PREFIX = 'n8n-nodes-'; diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index e7059e1159566..7ec8afb253441 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { CliWorkflowOperationError, SubworkflowOperationError } from 'n8n-workflow'; import type { INode } from 'n8n-workflow'; +import { START_NODES } from './constants'; function findWorkflowStart(executionMode: 'integrated' | 'cli') { return function (nodes: INode[]) { @@ -10,7 +11,7 @@ function findWorkflowStart(executionMode: 'integrated' | 'cli') { if (executeWorkflowTriggerNode) return executeWorkflowTriggerNode; - const startNode = nodes.find((node) => node.type === 'n8n-nodes-base.start'); + const startNode = nodes.find((node) => START_NODES.includes(node.type)); if (startNode) return startNode; From 6063c3cf9b0bc1be04edd0d7eadbeff534812ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Fri, 10 Feb 2023 14:38:29 +0100 Subject: [PATCH 2/2] initialize logger in command constructors --- packages/cli/src/commands/BaseCommand.ts | 6 +----- packages/cli/src/commands/db/revert.ts | 6 +----- packages/workflow/src/LoggerProxy.ts | 3 ++- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/commands/BaseCommand.ts b/packages/cli/src/commands/BaseCommand.ts index dfaf277ee1858..1ef136b209e3b 100644 --- a/packages/cli/src/commands/BaseCommand.ts +++ b/packages/cli/src/commands/BaseCommand.ts @@ -3,7 +3,6 @@ import type { INodeTypes } from 'n8n-workflow'; import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow'; import type { IUserSettings } from 'n8n-core'; import { BinaryDataManager, UserSettings } from 'n8n-core'; -import type { Logger } from '@/Logger'; import { getLogger } from '@/Logger'; import config from '@/config'; import * as Db from '@/Db'; @@ -23,7 +22,7 @@ export const UM_FIX_INSTRUCTION = 'Please fix the database by running ./packages/cli/bin/n8n user-management:reset'; export abstract class BaseCommand extends Command { - protected logger: Logger; + protected logger = LoggerProxy.init(getLogger()); protected externalHooks: IExternalHooksClass; @@ -34,9 +33,6 @@ export abstract class BaseCommand extends Command { protected userSettings: IUserSettings; async init(): Promise { - this.logger = getLogger(); - LoggerProxy.init(this.logger); - await initErrorHandling(); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment diff --git a/packages/cli/src/commands/db/revert.ts b/packages/cli/src/commands/db/revert.ts index 8e1ec167c21ca..56de3b566372a 100644 --- a/packages/cli/src/commands/db/revert.ts +++ b/packages/cli/src/commands/db/revert.ts @@ -2,7 +2,6 @@ import { Command, flags } from '@oclif/command'; import type { DataSourceOptions as ConnectionOptions } from 'typeorm'; import { DataSource as Connection } from 'typeorm'; import { LoggerProxy } from 'n8n-workflow'; -import type { Logger } from '@/Logger'; import { getLogger } from '@/Logger'; import { getConnectionOptions } from '@/Db'; import config from '@/config'; @@ -16,14 +15,11 @@ export class DbRevertMigrationCommand extends Command { help: flags.help({ char: 'h' }), }; - private logger: Logger; + protected logger = LoggerProxy.init(getLogger()); private connection: Connection; async init() { - this.logger = getLogger(); - LoggerProxy.init(this.logger); - this.parse(DbRevertMigrationCommand); } diff --git a/packages/workflow/src/LoggerProxy.ts b/packages/workflow/src/LoggerProxy.ts index c056013ddb71e..57cf6419c96b6 100644 --- a/packages/workflow/src/LoggerProxy.ts +++ b/packages/workflow/src/LoggerProxy.ts @@ -3,8 +3,9 @@ import type { ILogger, LogTypes } from './Interfaces'; let logger: ILogger | undefined; -export function init(loggerInstance: ILogger) { +export function init(loggerInstance: L) { logger = loggerInstance; + return loggerInstance; } export function getInstance(): ILogger {