mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
refactor(core): Overhaul commands setup. Add support for module commands (#16709)
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
committed by
GitHub
parent
346bc84093
commit
9f8d3d3bc8
116
packages/@n8n/decorators/src/command/__tests__/command.test.ts
Normal file
116
packages/@n8n/decorators/src/command/__tests__/command.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { Container } from '@n8n/di';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Command } from '../command';
|
||||
import { CommandMetadata } from '../command-metadata';
|
||||
|
||||
describe('@Command decorator', () => {
|
||||
let commandMetadata: CommandMetadata;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
Container.reset();
|
||||
|
||||
commandMetadata = new CommandMetadata();
|
||||
Container.set(CommandMetadata, commandMetadata);
|
||||
});
|
||||
|
||||
it('should register command in CommandMetadata', () => {
|
||||
@Command({
|
||||
name: 'test',
|
||||
description: 'Test command',
|
||||
examples: ['example usage'],
|
||||
flagsSchema: z.object({}),
|
||||
})
|
||||
class TestCommand {
|
||||
async run() {}
|
||||
}
|
||||
|
||||
const registeredCommands = commandMetadata.getEntries();
|
||||
|
||||
expect(registeredCommands).toHaveLength(1);
|
||||
const [commandName, entry] = registeredCommands[0];
|
||||
expect(commandName).toBe('test');
|
||||
expect(entry.class).toBe(TestCommand);
|
||||
});
|
||||
|
||||
it('should register multiple commands', () => {
|
||||
@Command({
|
||||
name: 'first-command',
|
||||
description: 'First test command',
|
||||
examples: ['example 1'],
|
||||
flagsSchema: z.object({}),
|
||||
})
|
||||
class FirstCommand {
|
||||
async run() {}
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'second-command',
|
||||
description: 'Second test command',
|
||||
examples: ['example 2'],
|
||||
flagsSchema: z.object({}),
|
||||
})
|
||||
class SecondCommand {
|
||||
async run() {}
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'third-command',
|
||||
description: 'Third test command',
|
||||
examples: ['example 3'],
|
||||
flagsSchema: z.object({}),
|
||||
})
|
||||
class ThirdCommand {
|
||||
async run() {}
|
||||
}
|
||||
|
||||
const registeredCommands = commandMetadata.getEntries();
|
||||
|
||||
expect(registeredCommands).toHaveLength(3);
|
||||
expect(commandMetadata.get('first-command')?.class).toBe(FirstCommand);
|
||||
expect(commandMetadata.get('second-command')?.class).toBe(SecondCommand);
|
||||
expect(commandMetadata.get('third-command')?.class).toBe(ThirdCommand);
|
||||
});
|
||||
|
||||
it('should apply Service decorator', () => {
|
||||
@Command({
|
||||
name: 'test',
|
||||
description: 'Test command',
|
||||
examples: ['example usage'],
|
||||
flagsSchema: z.object({}),
|
||||
})
|
||||
class TestCommand {
|
||||
async run() {}
|
||||
}
|
||||
|
||||
expect(Container.has(TestCommand)).toBe(true);
|
||||
});
|
||||
|
||||
it('stores the command metadata correctly', () => {
|
||||
const name = 'test-cmd';
|
||||
const description = 'Test command description';
|
||||
const examples = ['example 1', 'example 2'];
|
||||
const flagsSchema = z.object({
|
||||
flag1: z.string(),
|
||||
flag2: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name,
|
||||
description,
|
||||
examples,
|
||||
flagsSchema,
|
||||
})
|
||||
class TestCommand {
|
||||
async run() {}
|
||||
}
|
||||
|
||||
const entry = commandMetadata.get(name);
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry?.description).toBe(description);
|
||||
expect(entry?.examples).toEqual(examples);
|
||||
expect(entry?.flagsSchema).toBe(flagsSchema);
|
||||
expect(entry?.class).toBe(TestCommand);
|
||||
});
|
||||
});
|
||||
20
packages/@n8n/decorators/src/command/command-metadata.ts
Normal file
20
packages/@n8n/decorators/src/command/command-metadata.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Service } from '@n8n/di';
|
||||
|
||||
import type { CommandEntry } from './types';
|
||||
|
||||
@Service()
|
||||
export class CommandMetadata {
|
||||
private readonly commands: Map<string, CommandEntry> = new Map();
|
||||
|
||||
register(name: string, entry: CommandEntry) {
|
||||
this.commands.set(name, entry);
|
||||
}
|
||||
|
||||
get(name: string) {
|
||||
return this.commands.get(name);
|
||||
}
|
||||
|
||||
getEntries() {
|
||||
return [...this.commands.entries()];
|
||||
}
|
||||
}
|
||||
18
packages/@n8n/decorators/src/command/command.ts
Normal file
18
packages/@n8n/decorators/src/command/command.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Container, Service } from '@n8n/di';
|
||||
|
||||
import { CommandMetadata } from './command-metadata';
|
||||
import type { CommandClass, CommandOptions } from './types';
|
||||
|
||||
export const Command =
|
||||
({ name, description, examples, flagsSchema }: CommandOptions): ClassDecorator =>
|
||||
(target) => {
|
||||
const commandClass = target as unknown as CommandClass;
|
||||
Container.get(CommandMetadata).register(name, {
|
||||
description,
|
||||
flagsSchema,
|
||||
class: commandClass,
|
||||
examples,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return Service()(target);
|
||||
};
|
||||
3
packages/@n8n/decorators/src/command/index.ts
Normal file
3
packages/@n8n/decorators/src/command/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { Command } from './command';
|
||||
export { CommandMetadata } from './command-metadata';
|
||||
export type { ICommand, CommandClass, CommandEntry } from './types';
|
||||
28
packages/@n8n/decorators/src/command/types.ts
Normal file
28
packages/@n8n/decorators/src/command/types.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { Constructable } from '@n8n/di';
|
||||
import type { ZodObject, ZodTypeAny } from 'zod';
|
||||
|
||||
type FlagsSchema = ZodObject<Record<string, ZodTypeAny>>;
|
||||
|
||||
export type CommandOptions = {
|
||||
name: string;
|
||||
description: string;
|
||||
examples?: string[];
|
||||
flagsSchema?: FlagsSchema;
|
||||
};
|
||||
|
||||
export type ICommand = {
|
||||
flags?: object;
|
||||
init?: () => Promise<void>;
|
||||
run: () => Promise<void>;
|
||||
catch?: (e: Error) => Promise<void>;
|
||||
finally?: (e?: Error) => Promise<void>;
|
||||
};
|
||||
|
||||
export type CommandClass = Constructable<ICommand>;
|
||||
|
||||
export type CommandEntry = {
|
||||
class: CommandClass;
|
||||
description: string;
|
||||
examples?: string[];
|
||||
flagsSchema?: FlagsSchema;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './controller';
|
||||
export * from './command';
|
||||
export { Debounce } from './debounce';
|
||||
export * from './execution-lifecycle';
|
||||
export { Memoized } from './memoized';
|
||||
|
||||
@@ -13,11 +13,6 @@ if (versionFlags.includes(process.argv.slice(-1)[0])) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (process.argv.length === 2) {
|
||||
// When no command is given choose by default start
|
||||
process.argv.push('start');
|
||||
}
|
||||
|
||||
const satisfies = require('semver/functions/satisfies');
|
||||
const nodeVersion = process.versions.node;
|
||||
const {
|
||||
@@ -63,10 +58,7 @@ if (process.env.NODEJS_PREFER_IPV4 === 'true') {
|
||||
require('net').setDefaultAutoSelectFamily?.(false);
|
||||
|
||||
(async () => {
|
||||
// Collect DB entities from modules _before_ `DbConnectionOptions` is instantiated.
|
||||
const { BaseCommand } = await import('../dist/commands/base-command.js');
|
||||
await new BaseCommand([], { root: __dirname }).loadModules();
|
||||
|
||||
const oclif = await import('@oclif/core');
|
||||
await oclif.execute({ dir: __dirname });
|
||||
const { Container } = await import('@n8n/di');
|
||||
const { CommandRegistry } = await import('../dist/command-registry.js');
|
||||
await Container.get(CommandRegistry).execute();
|
||||
})();
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
"description": "n8n Workflow Automation Tool",
|
||||
"main": "dist/index",
|
||||
"types": "dist/index.d.ts",
|
||||
"oclif": {
|
||||
"commands": "./dist/commands",
|
||||
"helpClass": "./dist/help",
|
||||
"bin": "n8n"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"typecheck": "tsc --noEmit",
|
||||
@@ -77,6 +72,7 @@
|
||||
"@types/ws": "^8.18.1",
|
||||
"@types/xml2js": "catalog:",
|
||||
"@types/yamljs": "^0.2.31",
|
||||
"@types/yargs-parser": "21.0.0",
|
||||
"@vvo/tzdb": "^6.141.0",
|
||||
"concurrently": "^8.2.0",
|
||||
"ioredis-mock": "^8.8.1",
|
||||
@@ -106,7 +102,6 @@
|
||||
"@n8n/typeorm": "catalog:",
|
||||
"@n8n_io/ai-assistant-sdk": "catalog:",
|
||||
"@n8n_io/license-sdk": "2.22.0",
|
||||
"@oclif/core": "4.0.7",
|
||||
"@rudderstack/rudder-sdk-node": "2.1.4",
|
||||
"@sentry/node": "catalog:",
|
||||
"aws4": "1.11.0",
|
||||
@@ -180,6 +175,7 @@
|
||||
"xmllint-wasm": "3.0.1",
|
||||
"xss": "catalog:",
|
||||
"yamljs": "0.3.0",
|
||||
"yargs-parser": "21.1.1",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
166
packages/cli/src/__tests__/command-registry.test.ts
Normal file
166
packages/cli/src/__tests__/command-registry.test.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import type { Logger, ModuleRegistry } from '@n8n/backend-common';
|
||||
import { CommandMetadata } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CommandRegistry } from '../command-registry';
|
||||
|
||||
jest.mock('fast-glob');
|
||||
// eslint-disable-next-line import-x/no-default-export
|
||||
import glob from 'fast-glob';
|
||||
|
||||
describe('CommandRegistry', () => {
|
||||
let commandRegistry: CommandRegistry;
|
||||
let commandMetadata: CommandMetadata;
|
||||
const moduleRegistry = mock<ModuleRegistry>();
|
||||
const logger = mock<Logger>();
|
||||
let originalProcessArgv: string[];
|
||||
let mockProcessExit: jest.SpyInstance;
|
||||
|
||||
class TestCommand {
|
||||
flags: any;
|
||||
|
||||
init = jest.fn();
|
||||
|
||||
run = jest.fn();
|
||||
|
||||
catch = jest.fn();
|
||||
|
||||
finally = jest.fn();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
originalProcessArgv = process.argv;
|
||||
mockProcessExit = jest.spyOn(process, 'exit').mockImplementation((() => {}) as any);
|
||||
|
||||
(glob as unknown as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
commandMetadata = new CommandMetadata();
|
||||
Container.set(CommandMetadata, commandMetadata);
|
||||
|
||||
commandMetadata.register('test-command', {
|
||||
description: 'Test command description',
|
||||
class: TestCommand,
|
||||
examples: ['--example'],
|
||||
flagsSchema: z.object({
|
||||
flag1: z.string().describe('Flag one description').optional(),
|
||||
flag2: z.boolean().describe('Flag two description').optional(),
|
||||
shortFlag: z.number().alias('s').describe('Short flag with alias').optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
Container.set(TestCommand, new TestCommand());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.argv = originalProcessArgv;
|
||||
mockProcessExit.mockRestore();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should execute the specified command', async () => {
|
||||
process.argv = ['node', 'n8n', 'test-command'];
|
||||
|
||||
commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger);
|
||||
await commandRegistry.execute();
|
||||
|
||||
expect(moduleRegistry.loadModules).toHaveBeenCalled();
|
||||
|
||||
const commandInstance = Container.get(commandMetadata.get('test-command')!.class);
|
||||
expect(commandInstance.init).toHaveBeenCalled();
|
||||
expect(commandInstance.run).toHaveBeenCalled();
|
||||
expect(commandInstance.finally).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('should handle command errors', async () => {
|
||||
process.argv = ['node', 'n8n', 'test-command'];
|
||||
|
||||
const error = new Error('Test error');
|
||||
const commandClass = commandMetadata.get('test-command')!.class;
|
||||
const commandInstance = Container.get(commandClass);
|
||||
commandInstance.run = jest.fn().mockRejectedValue(error);
|
||||
|
||||
commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger);
|
||||
await commandRegistry.execute();
|
||||
|
||||
expect(commandInstance.catch).toHaveBeenCalledWith(error);
|
||||
expect(commandInstance.finally).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
it('should parse and apply command flags', async () => {
|
||||
process.argv = ['node', 'n8n', 'test-command', '--flag1', 'value1', '--flag2', '-s', '123'];
|
||||
|
||||
commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger);
|
||||
await commandRegistry.execute();
|
||||
|
||||
const commandInstance = Container.get(commandMetadata.get('test-command')!.class);
|
||||
expect(commandInstance.flags).toEqual({
|
||||
flag1: 'value1',
|
||||
flag2: true,
|
||||
shortFlag: 123,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle alias flags', async () => {
|
||||
process.argv = ['node', 'n8n', 'test-command', '--flag1', 'value1', '-s', '123'];
|
||||
|
||||
commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger);
|
||||
await commandRegistry.execute();
|
||||
|
||||
const commandInstance = Container.get(commandMetadata.get('test-command')!.class);
|
||||
expect(commandInstance.flags).toEqual({
|
||||
flag1: 'value1',
|
||||
shortFlag: 123,
|
||||
});
|
||||
});
|
||||
|
||||
it('should exit with error when command not found', async () => {
|
||||
process.argv = ['node', 'n8n', 'non-existent-command'];
|
||||
|
||||
commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger);
|
||||
await commandRegistry.execute();
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('not found'));
|
||||
expect(mockProcessExit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should display help when --help flag is used', async () => {
|
||||
process.argv = ['node', 'n8n', 'test-command', '--help'];
|
||||
|
||||
commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger);
|
||||
await commandRegistry.execute();
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('USAGE'));
|
||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('FLAGS'));
|
||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('DESCRIPTION'));
|
||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('EXAMPLES'));
|
||||
expect(mockProcessExit).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('should list all commands when global help is requested', async () => {
|
||||
process.argv = ['node', 'n8n', '--help'];
|
||||
|
||||
commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger);
|
||||
await commandRegistry.execute();
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Available commands'));
|
||||
expect(mockProcessExit).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('should display proper command usage with printCommandUsage', () => {
|
||||
process.argv = ['node', 'n8n', 'test-command'];
|
||||
|
||||
commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger);
|
||||
const commandEntry = commandMetadata.get('test-command')!;
|
||||
commandRegistry.printCommandUsage(commandEntry);
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('USAGE'));
|
||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('test-command'));
|
||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('--flag1 <value>'));
|
||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('-s, --shortFlag <value>'));
|
||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Flag one description'));
|
||||
});
|
||||
});
|
||||
184
packages/cli/src/command-registry.ts
Normal file
184
packages/cli/src/command-registry.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { Logger, ModuleRegistry } from '@n8n/backend-common';
|
||||
import { CommandMetadata, type CommandEntry } from '@n8n/decorators';
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import glob from 'fast-glob';
|
||||
import picocolors from 'picocolors';
|
||||
import argvParser from 'yargs-parser';
|
||||
import { z } from 'zod';
|
||||
import './zod-alias-support';
|
||||
|
||||
/**
|
||||
* Registry that manages CLI commands, their execution, and metadata.
|
||||
* Handles command discovery, flag parsing, and execution lifecycle.
|
||||
*/
|
||||
@Service()
|
||||
export class CommandRegistry {
|
||||
private commandName: string;
|
||||
|
||||
private readonly argv: argvParser.Arguments;
|
||||
|
||||
constructor(
|
||||
private readonly commandMetadata: CommandMetadata,
|
||||
private readonly moduleRegistry: ModuleRegistry,
|
||||
private readonly logger: Logger,
|
||||
) {
|
||||
this.argv = argvParser(process.argv.slice(2));
|
||||
this.commandName = process.argv[2] ?? 'start';
|
||||
}
|
||||
|
||||
async execute() {
|
||||
if (this.commandName === '--help' || this.commandName === '-h') {
|
||||
await this.listAllCommands();
|
||||
return process.exit(0);
|
||||
}
|
||||
|
||||
if (this.commandName === 'executeBatch') {
|
||||
this.logger.warn('WARNING: "executeBatch" has been renamed to "execute-batch".');
|
||||
this.commandName = 'execute-batch';
|
||||
}
|
||||
|
||||
// Try to load regular commands
|
||||
try {
|
||||
await import(`./commands/${this.commandName.replaceAll(':', '/')}.js`);
|
||||
} catch {}
|
||||
|
||||
// Load modules to ensure all module commands are registered
|
||||
await this.moduleRegistry.loadModules();
|
||||
|
||||
const commandEntry = this.commandMetadata.get(this.commandName);
|
||||
if (!commandEntry) {
|
||||
this.logger.error(picocolors.red(`Error: Command "${this.commandName}" not found`));
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
if (this.argv.help || this.argv.h) {
|
||||
this.printCommandUsage(commandEntry);
|
||||
return process.exit(0);
|
||||
}
|
||||
|
||||
const command = Container.get(commandEntry.class);
|
||||
command.flags = this.parseFlags(commandEntry);
|
||||
|
||||
let error: Error | undefined = undefined;
|
||||
try {
|
||||
await command.init?.();
|
||||
await command.run();
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
await command.catch?.(error);
|
||||
} finally {
|
||||
await command.finally?.(error);
|
||||
}
|
||||
}
|
||||
|
||||
parseFlags(commandEntry: CommandEntry) {
|
||||
if (!commandEntry.flagsSchema) return {};
|
||||
const { _, ...argv } = this.argv;
|
||||
Object.entries(commandEntry.flagsSchema.shape).forEach(([key, flagSchema]) => {
|
||||
let schemaDef = flagSchema._def as z.ZodTypeDef & {
|
||||
typeName: string;
|
||||
innerType?: z.ZodType;
|
||||
};
|
||||
if (schemaDef.typeName === 'ZodOptional' && schemaDef.innerType) {
|
||||
schemaDef = schemaDef.innerType._def as typeof schemaDef;
|
||||
}
|
||||
const alias = schemaDef._alias;
|
||||
if (alias?.length && !(key in argv) && argv[alias]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
argv[key] = argv[alias];
|
||||
}
|
||||
});
|
||||
return commandEntry.flagsSchema.parse(argv);
|
||||
}
|
||||
|
||||
async listAllCommands() {
|
||||
// Import all command files to register all the non-module commands
|
||||
const commandFiles = await glob('./commands/**/*.js', {
|
||||
ignore: ['**/__tests__/**'],
|
||||
cwd: __dirname,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
await Promise.all(commandFiles.map(async (filePath) => await import(filePath)));
|
||||
|
||||
// Load/List module commands after legacy commands
|
||||
await this.moduleRegistry.loadModules();
|
||||
|
||||
this.logger.info('Available commands:');
|
||||
|
||||
for (const [name, { description }] of this.commandMetadata.getEntries()) {
|
||||
this.logger.info(
|
||||
` ${picocolors.bold(picocolors.green(name))}: \n ${description.split('\n')[0]}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
'\nFor more detailed information, visit:\nhttps://docs.n8n.io/hosting/cli-commands/',
|
||||
);
|
||||
}
|
||||
|
||||
printCommandUsage(commandEntry: CommandEntry) {
|
||||
const { commandName } = this;
|
||||
let output = '';
|
||||
|
||||
output += `${picocolors.bold('USAGE')}\n`;
|
||||
output += ` $ n8n ${commandName}\n\n`;
|
||||
|
||||
const { flagsSchema } = commandEntry;
|
||||
if (flagsSchema && Object.keys(flagsSchema.shape).length > 0) {
|
||||
const flagLines: Array<[string, string]> = [];
|
||||
const flagEntries = Object.entries(
|
||||
z
|
||||
.object({
|
||||
help: z.boolean().alias('h').describe('Show CLI help'),
|
||||
})
|
||||
.merge(flagsSchema).shape,
|
||||
);
|
||||
for (const [flagName, flagSchema] of flagEntries) {
|
||||
let schemaDef = flagSchema._def as z.ZodTypeDef & {
|
||||
typeName: string;
|
||||
innerType?: z.ZodType;
|
||||
};
|
||||
if (schemaDef.typeName === 'ZodOptional' && schemaDef.innerType) {
|
||||
schemaDef = schemaDef.innerType._def as typeof schemaDef;
|
||||
}
|
||||
const typeName = schemaDef.typeName;
|
||||
|
||||
let flagString = `--${flagName}`;
|
||||
if (schemaDef._alias) {
|
||||
flagString = `-${schemaDef._alias}, ${flagString}`;
|
||||
}
|
||||
if (['ZodString', 'ZodNumber', 'ZodArray'].includes(typeName)) {
|
||||
flagString += ' <value>';
|
||||
}
|
||||
|
||||
let flagLine = flagSchema.description ?? '';
|
||||
if ('defaultValue' in schemaDef) {
|
||||
const defaultValue = (schemaDef as z.ZodDefaultDef).defaultValue() as unknown;
|
||||
flagLine += ` [default: ${String(defaultValue)}]`;
|
||||
}
|
||||
flagLines.push([flagString, flagLine]);
|
||||
}
|
||||
|
||||
const flagColumnWidth = Math.max(...flagLines.map(([flagString]) => flagString.length));
|
||||
|
||||
output += `${picocolors.bold('FLAGS')}\n`;
|
||||
output += flagLines
|
||||
.map(([flagString, flagLine]) => ` ${flagString.padEnd(flagColumnWidth)} ${flagLine}`)
|
||||
.join('\n');
|
||||
output += '\n\n';
|
||||
}
|
||||
|
||||
output += `${picocolors.bold('DESCRIPTION')}\n`;
|
||||
output += ` ${commandEntry.description}\n`;
|
||||
|
||||
if (commandEntry.examples?.length) {
|
||||
output += `\n${picocolors.bold('EXAMPLES')}\n`;
|
||||
output += commandEntry.examples
|
||||
.map((example) => ` $ n8n ${commandName}${example ? ` ${example}` : ''}`)
|
||||
.join('\n');
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
this.logger.info(output);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type InstalledNodes } from '@n8n/db';
|
||||
import { type CredentialsEntity } from '@n8n/db';
|
||||
import { type User } from '@n8n/db';
|
||||
import { type Config } from '@oclif/core';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
|
||||
import { CommunityNode } from '../community-node';
|
||||
@@ -9,8 +8,7 @@ import { CommunityNode } from '../community-node';
|
||||
describe('uninstallCredential', () => {
|
||||
const userId = '1234';
|
||||
|
||||
const config: Config = mock<Config>();
|
||||
const communityNode = new CommunityNode(['--uninstall', '--credential', 'evolutionApi'], config);
|
||||
const communityNode = new CommunityNode();
|
||||
|
||||
beforeEach(() => {
|
||||
communityNode.deleteCredential = jest.fn();
|
||||
@@ -31,9 +29,8 @@ describe('uninstallCredential', () => {
|
||||
const user = mock<User>();
|
||||
const credentials = [credential];
|
||||
|
||||
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||
flags: { credential: credentialType, uninstall: true, userId },
|
||||
});
|
||||
// @ts-expect-error Protected property
|
||||
communityNode.flags = { credential: credentialType, uninstall: true, userId };
|
||||
communityNode.findCredentialsByType = jest.fn().mockReturnValue(credentials);
|
||||
communityNode.findUserById = jest.fn().mockReturnValue(user);
|
||||
|
||||
@@ -59,9 +56,8 @@ describe('uninstallCredential', () => {
|
||||
const credential = mock<CredentialsEntity>();
|
||||
credential.id = '666';
|
||||
|
||||
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||
flags: { credential: credentialType, uninstall: true, userId },
|
||||
});
|
||||
// @ts-expect-error Protected property
|
||||
communityNode.flags = { credential: credentialType, uninstall: true, userId };
|
||||
communityNode.findUserById = jest.fn().mockReturnValue(null);
|
||||
|
||||
const deleteCredential = jest.spyOn(communityNode, 'deleteCredential');
|
||||
@@ -83,9 +79,8 @@ describe('uninstallCredential', () => {
|
||||
const credential = mock<CredentialsEntity>();
|
||||
credential.id = '666';
|
||||
|
||||
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||
flags: { credential: credentialType, uninstall: true, userId },
|
||||
});
|
||||
// @ts-expect-error Protected property
|
||||
communityNode.flags = { credential: credentialType, uninstall: true, userId };
|
||||
communityNode.findUserById = jest.fn().mockReturnValue(mock<User>());
|
||||
communityNode.findCredentialsByType = jest.fn().mockReturnValue(null);
|
||||
|
||||
@@ -116,9 +111,8 @@ describe('uninstallCredential', () => {
|
||||
const user = mock<User>();
|
||||
const credentials = [credential1, credential2];
|
||||
|
||||
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||
flags: { credential: credentialType, uninstall: true, userId },
|
||||
});
|
||||
// @ts-expect-error Protected property
|
||||
communityNode.flags = { credential: credentialType, uninstall: true, userId };
|
||||
communityNode.findCredentialsByType = jest.fn().mockReturnValue(credentials);
|
||||
communityNode.findUserById = jest.fn().mockReturnValue(user);
|
||||
|
||||
@@ -141,11 +135,7 @@ describe('uninstallCredential', () => {
|
||||
});
|
||||
|
||||
describe('uninstallPackage', () => {
|
||||
const config: Config = mock<Config>();
|
||||
const communityNode = new CommunityNode(
|
||||
['--uninstall', '--package', 'n8n-nodes-evolution-api.evolutionApi'],
|
||||
config,
|
||||
);
|
||||
const communityNode = new CommunityNode();
|
||||
|
||||
beforeEach(() => {
|
||||
communityNode.removeCommunityPackage = jest.fn();
|
||||
@@ -164,9 +154,8 @@ describe('uninstallPackage', () => {
|
||||
installedNodes: [installedNode],
|
||||
};
|
||||
|
||||
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||
flags: { package: 'n8n-nodes-evolution-api', uninstall: true },
|
||||
});
|
||||
// @ts-expect-error Protected property
|
||||
communityNode.flags = { package: 'n8n-nodes-evolution-api', uninstall: true };
|
||||
communityNode.findCommunityPackage = jest.fn().mockReturnValue(communityPackage);
|
||||
|
||||
const deleteCommunityNode = jest.spyOn(communityNode, 'deleteCommunityNode');
|
||||
@@ -196,9 +185,8 @@ describe('uninstallPackage', () => {
|
||||
installedNodes: [installedNode0, installedNode1],
|
||||
};
|
||||
|
||||
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||
flags: { package: 'n8n-nodes-evolution-api', uninstall: true },
|
||||
});
|
||||
// @ts-expect-error Protected property
|
||||
communityNode.flags = { package: 'n8n-nodes-evolution-api', uninstall: true };
|
||||
communityNode.findCommunityPackage = jest.fn().mockReturnValue(communityPackage);
|
||||
|
||||
const deleteCommunityNode = jest.spyOn(communityNode, 'deleteCommunityNode');
|
||||
@@ -222,9 +210,8 @@ describe('uninstallPackage', () => {
|
||||
});
|
||||
|
||||
it('should return if a package is not found', async () => {
|
||||
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||
flags: { package: 'n8n-nodes-evolution-api', uninstall: true },
|
||||
});
|
||||
// @ts-expect-error Protected property
|
||||
communityNode.flags = { package: 'n8n-nodes-evolution-api', uninstall: true };
|
||||
communityNode.findCommunityPackage = jest.fn().mockReturnValue(null);
|
||||
|
||||
const deleteCommunityNode = jest.spyOn(communityNode, 'deleteCommunityNode');
|
||||
@@ -246,9 +233,8 @@ describe('uninstallPackage', () => {
|
||||
installedNodes: [],
|
||||
};
|
||||
|
||||
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||
flags: { package: 'n8n-nodes-evolution-api', uninstall: true },
|
||||
});
|
||||
// @ts-expect-error Protected property
|
||||
communityNode.flags = { package: 'n8n-nodes-evolution-api', uninstall: true };
|
||||
communityNode.findCommunityPackage = jest.fn().mockReturnValue(communityPackage);
|
||||
|
||||
const deleteCommunityNode = jest.spyOn(communityNode, 'deleteCommunityNode');
|
||||
|
||||
@@ -5,7 +5,6 @@ import { WorkflowRepository } from '@n8n/db';
|
||||
import { DbConnection } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import type { Config } from '@oclif/core';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IRun } from 'n8n-workflow';
|
||||
|
||||
@@ -75,9 +74,9 @@ test('should start a task runner when task runners are enabled', async () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const cmd = new ExecuteBatch([], {} as Config);
|
||||
// @ts-expect-error Private property
|
||||
cmd.parse = jest.fn().mockResolvedValue({ flags: {} });
|
||||
const cmd = new ExecuteBatch();
|
||||
// @ts-expect-error Protected property
|
||||
cmd.flags = {};
|
||||
// @ts-expect-error Private property
|
||||
cmd.runTests = jest.fn().mockResolvedValue({ summary: { failedExecutions: [] } });
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { User, WorkflowEntity } from '@n8n/db';
|
||||
import { WorkflowRepository } from '@n8n/db';
|
||||
import { DbConnection } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { Config } from '@oclif/core';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IRun } from 'n8n-workflow';
|
||||
|
||||
@@ -69,9 +68,9 @@ test('should start a task runner when task runners are enabled', async () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const cmd = new Execute([], {} as Config);
|
||||
// @ts-expect-error Private property
|
||||
cmd.parse = jest.fn().mockResolvedValue({ flags: { id: '123' } });
|
||||
const cmd = new Execute();
|
||||
// @ts-expect-error Protected property
|
||||
cmd.flags = { id: '123' };
|
||||
|
||||
// act
|
||||
|
||||
|
||||
@@ -1,38 +1,35 @@
|
||||
import { SecurityConfig } from '@n8n/config';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Flags } from '@oclif/core';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import z from 'zod';
|
||||
|
||||
import { RISK_CATEGORIES } from '@/security-audit/constants';
|
||||
import type { Risk } from '@/security-audit/types';
|
||||
|
||||
import { BaseCommand } from './base-command';
|
||||
|
||||
export class SecurityAudit extends BaseCommand {
|
||||
static description = 'Generate a security audit report for this n8n instance';
|
||||
|
||||
static examples = [
|
||||
'$ n8n audit',
|
||||
'$ n8n audit --categories=database,credentials',
|
||||
'$ n8n audit --days-abandoned-workflow=10',
|
||||
];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
categories: Flags.string({
|
||||
default: RISK_CATEGORIES.join(','),
|
||||
description: 'Comma-separated list of categories to include in the audit',
|
||||
}),
|
||||
|
||||
'days-abandoned-workflow': Flags.integer({
|
||||
default: Container.get(SecurityConfig).daysAbandonedWorkflow,
|
||||
description: 'Days for a workflow to be considered abandoned if not executed',
|
||||
}),
|
||||
};
|
||||
const flagsSchema = z.object({
|
||||
categories: z
|
||||
.string()
|
||||
.default(RISK_CATEGORIES.join(','))
|
||||
.describe('Comma-separated list of categories to include in the audit'),
|
||||
'days-abandoned-workflow': z
|
||||
.number()
|
||||
.int()
|
||||
.default(Container.get(SecurityConfig).daysAbandonedWorkflow)
|
||||
.describe('Days for a workflow to be considered abandoned if not executed'),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'audit',
|
||||
description: 'Generate a security audit report for this n8n instance',
|
||||
examples: ['', '--categories=database,credentials', '--days-abandoned-workflow=10'],
|
||||
flagsSchema,
|
||||
})
|
||||
export class SecurityAudit extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
async run() {
|
||||
const { flags: auditFlags } = await this.parse(SecurityAudit);
|
||||
|
||||
const { flags: auditFlags } = this;
|
||||
const categories =
|
||||
auditFlags.categories?.split(',').filter((c): c is Risk.Category => c !== '') ??
|
||||
RISK_CATEGORIES;
|
||||
|
||||
@@ -11,7 +11,6 @@ import { GlobalConfig } from '@n8n/config';
|
||||
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||
import { DbConnection } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Command, Errors } from '@oclif/core';
|
||||
import {
|
||||
BinaryDataConfig,
|
||||
BinaryDataService,
|
||||
@@ -20,7 +19,7 @@ import {
|
||||
DataDeduplicationService,
|
||||
ErrorReporter,
|
||||
} from 'n8n-core';
|
||||
import { ensureError, sleep, UserError } from 'n8n-workflow';
|
||||
import { ensureError, sleep, UnexpectedError, UserError } from 'n8n-workflow';
|
||||
|
||||
import type { AbstractServer } from '@/abstract-server';
|
||||
import config from '@/config';
|
||||
@@ -39,7 +38,9 @@ import { PostHogClient } from '@/posthog';
|
||||
import { ShutdownService } from '@/shutdown/shutdown.service';
|
||||
import { WorkflowHistoryManager } from '@/workflows/workflow-history.ee/workflow-history-manager.ee';
|
||||
|
||||
export abstract class BaseCommand extends Command {
|
||||
export abstract class BaseCommand<F = never> {
|
||||
readonly flags: F;
|
||||
|
||||
protected logger = Container.get(Logger);
|
||||
|
||||
protected dbConnection: DbConnection;
|
||||
@@ -76,10 +77,6 @@ export abstract class BaseCommand extends Command {
|
||||
/** Whether to init task runner (if enabled). */
|
||||
protected needsTaskRunner = false;
|
||||
|
||||
protected async loadModules() {
|
||||
await this.moduleRegistry.loadModules();
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.dbConnection = Container.get(DbConnection);
|
||||
this.errorReporter = Container.get(ErrorReporter);
|
||||
@@ -175,6 +172,14 @@ export abstract class BaseCommand extends Command {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
protected log(message: string) {
|
||||
this.logger.info(message);
|
||||
}
|
||||
|
||||
protected error(message: string) {
|
||||
throw new UnexpectedError(message);
|
||||
}
|
||||
|
||||
async initObjectStoreService() {
|
||||
const binaryDataConfig = Container.get(BinaryDataConfig);
|
||||
const isSelected = binaryDataConfig.mode === 's3';
|
||||
@@ -193,7 +198,7 @@ export abstract class BaseCommand extends Command {
|
||||
this.logger.error(
|
||||
'No license found for S3 storage. \n Either set `N8N_DEFAULT_BINARY_DATA_MODE` to something else, or upgrade to a license that supports this feature.',
|
||||
);
|
||||
return this.exit(1);
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
this.logger.debug('License found for external storage - Initializing object store service');
|
||||
@@ -264,13 +269,12 @@ export abstract class BaseCommand extends Command {
|
||||
|
||||
async finally(error: Error | undefined) {
|
||||
if (error?.message) this.logger.error(error.message);
|
||||
if (inTest || this.id === 'start') return;
|
||||
if (inTest || this.constructor.name === 'Start') return;
|
||||
if (this.dbConnection.connectionState.connected) {
|
||||
await sleep(100); // give any in-flight query some time to finish
|
||||
await this.dbConnection.close();
|
||||
}
|
||||
const exitCode = error instanceof Errors.ExitError ? error.oclif.exit : error ? 1 : 0;
|
||||
this.exit(exitCode);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
protected onTerminationSignal(signal: string) {
|
||||
|
||||
@@ -1,45 +1,43 @@
|
||||
import { type InstalledNodes, type InstalledPackages, type User } from '@n8n/db';
|
||||
import { CredentialsRepository, InstalledNodesRepository, UserRepository } from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Flags } from '@oclif/core';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CredentialsService } from '@/credentials/credentials.service';
|
||||
import { CommunityPackagesService } from '@/services/community-packages.service';
|
||||
|
||||
import { BaseCommand } from './base-command';
|
||||
|
||||
export class CommunityNode extends BaseCommand {
|
||||
static description = '\nUninstall a community node and its credentials';
|
||||
|
||||
static examples = [
|
||||
'$ n8n community-node --uninstall --package n8n-nodes-evolution-api',
|
||||
'$ n8n community-node --uninstall --credential evolutionApi --userId 1234',
|
||||
];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
uninstall: Flags.boolean({
|
||||
description: 'Uninstalls the node',
|
||||
}),
|
||||
package: Flags.string({
|
||||
description: 'Package name of the community node.',
|
||||
}),
|
||||
credential: Flags.string({
|
||||
description:
|
||||
"Type of the credential.\nGet this value by visiting the node's .credential.ts file and getting the value of `name`",
|
||||
}),
|
||||
userId: Flags.string({
|
||||
description:
|
||||
'The ID of the user who owns the credential.\nOn self-hosted, query the database.\nOn cloud, query the API with your API key',
|
||||
}),
|
||||
};
|
||||
|
||||
async init() {
|
||||
await super.init();
|
||||
}
|
||||
const flagsSchema = z.object({
|
||||
uninstall: z.boolean().describe('Uninstalls the node').optional(),
|
||||
package: z.string().describe('Package name of the community node.').optional(),
|
||||
credential: z
|
||||
.string()
|
||||
.describe(
|
||||
"Type of the credential.\nGet this value by visiting the node's .credential.ts file and getting the value of `name`",
|
||||
)
|
||||
.optional(),
|
||||
userId: z
|
||||
.string()
|
||||
.describe(
|
||||
'The ID of the user who owns the credential.\nOn self-hosted, query the database.\nOn cloud, query the API with your API key',
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'community-node',
|
||||
description: 'Uninstall a community node and its credentials',
|
||||
examples: [
|
||||
'--uninstall --package n8n-nodes-evolution-api',
|
||||
'--uninstall --credential evolutionApi --userId 1234',
|
||||
],
|
||||
flagsSchema,
|
||||
})
|
||||
export class CommunityNode extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
async run() {
|
||||
const { flags } = await this.parseFlags();
|
||||
const { flags } = this;
|
||||
|
||||
const packageName = flags.package;
|
||||
const credentialType = flags.credential;
|
||||
@@ -139,10 +137,6 @@ export class CommunityNode extends BaseCommand {
|
||||
await Container.get(CommunityPackagesService).executeNpmCommand('npm prune');
|
||||
}
|
||||
|
||||
async parseFlags() {
|
||||
return await this.parse(CommunityNode);
|
||||
}
|
||||
|
||||
async deleteCommunityNode(node: InstalledNodes) {
|
||||
return await Container.get(InstalledNodesRepository).delete({
|
||||
type: node.type,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import type { Migration } from '@n8n/db';
|
||||
import { wrapMigration, DbConnectionOptions } from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import type { DataSourceOptions as ConnectionOptions } from '@n8n/typeorm';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { MigrationExecutor, DataSource as Connection } from '@n8n/typeorm';
|
||||
import { Command, Flags } from '@oclif/core';
|
||||
|
||||
// This function is extracted to make it easier to unit test it.
|
||||
// Mocking turned into a mess due to this command using typeorm and the db
|
||||
@@ -59,22 +59,14 @@ export async function main(
|
||||
await connection.destroy();
|
||||
}
|
||||
|
||||
export class DbRevertMigrationCommand extends Command {
|
||||
static description = 'Revert last database migration';
|
||||
|
||||
static examples = ['$ n8n db:revert'];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
};
|
||||
|
||||
protected logger = Container.get(Logger);
|
||||
|
||||
@Command({
|
||||
name: 'db:revert',
|
||||
description: 'Revert last database migration',
|
||||
})
|
||||
export class DbRevertMigrationCommand {
|
||||
private connection: Connection;
|
||||
|
||||
async init() {
|
||||
await this.parse(DbRevertMigrationCommand);
|
||||
}
|
||||
constructor(private readonly logger: Logger) {}
|
||||
|
||||
async run() {
|
||||
const connectionOptions: ConnectionOptions = {
|
||||
@@ -104,6 +96,6 @@ export class DbRevertMigrationCommand extends Command {
|
||||
protected async finally(error: Error | undefined) {
|
||||
if (this.connection?.isInitialized) await this.connection.destroy();
|
||||
|
||||
this.exit(error ? 1 : 0);
|
||||
process.exit(error ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-loop-func */
|
||||
import type { User } from '@n8n/db';
|
||||
import { WorkflowRepository } from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Flags } from '@oclif/core';
|
||||
import fs from 'fs';
|
||||
import { diff } from 'json-diff';
|
||||
import pick from 'lodash/pick';
|
||||
@@ -10,6 +10,7 @@ import type { IRun, ITaskData, IWorkflowBase, IWorkflowExecutionDataProcess } fr
|
||||
import { jsonParse, UnexpectedError } from 'n8n-workflow';
|
||||
import os from 'os';
|
||||
import { sep } from 'path';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ActiveExecutions } from '@/active-executions';
|
||||
import { OwnershipService } from '@/services/ownership.service';
|
||||
@@ -35,9 +36,81 @@ interface ISkipList {
|
||||
ticketReference: string;
|
||||
}
|
||||
|
||||
export class ExecuteBatch extends BaseCommand {
|
||||
static description = '\nExecutes multiple workflows once';
|
||||
const flagsSchema = z.object({
|
||||
debug: z
|
||||
.boolean()
|
||||
.describe('Toggles on displaying all errors and debug messages.')
|
||||
.default(false),
|
||||
ids: z
|
||||
.string()
|
||||
.describe(
|
||||
'Specifies workflow IDs to get executed, separated by a comma or a file containing the ids',
|
||||
)
|
||||
.optional(),
|
||||
concurrency: z
|
||||
.number()
|
||||
.int()
|
||||
.default(1)
|
||||
.describe('How many workflows can run in parallel. Defaults to 1 which means no concurrency.'),
|
||||
output: z
|
||||
.string()
|
||||
.describe(
|
||||
'Enable execution saving, You must inform an existing folder to save execution via this param',
|
||||
)
|
||||
.optional(),
|
||||
snapshot: z
|
||||
.string()
|
||||
.describe(
|
||||
'Enables snapshot saving. You must inform an existing folder to save snapshots via this param.',
|
||||
)
|
||||
.optional(),
|
||||
compare: z
|
||||
.string()
|
||||
.describe(
|
||||
'Compares current execution with an existing snapshot. You must inform an existing folder where the snapshots are saved.',
|
||||
)
|
||||
.optional(),
|
||||
shallow: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'Compares only if attributes output from node are the same, with no regards to nested JSON objects.',
|
||||
)
|
||||
.optional(),
|
||||
githubWorkflow: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'Enables more lenient comparison for GitHub workflows. This is useful for reducing false positives when comparing Test workflows.',
|
||||
)
|
||||
.optional(),
|
||||
skipList: z
|
||||
.string()
|
||||
.describe('File containing a comma separated list of workflow IDs to skip.')
|
||||
.optional(),
|
||||
retries: z
|
||||
.number()
|
||||
.int()
|
||||
.default(1)
|
||||
.describe('Retries failed workflows up to N tries. Default is 1. Set 0 to disable.'),
|
||||
shortOutput: z
|
||||
.boolean()
|
||||
.describe('Omits the full execution information from output, displaying only summary.')
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'execute-batch',
|
||||
description: 'Executes multiple workflows once',
|
||||
examples: [
|
||||
'',
|
||||
'--concurrency=10 --skipList=/data/skipList.json',
|
||||
'--debug --output=/data/output.json',
|
||||
'--ids=10,13,15 --shortOutput',
|
||||
'--snapshot=/data/snapshots --shallow',
|
||||
'--compare=/data/previousExecutionData --retries=2',
|
||||
],
|
||||
flagsSchema,
|
||||
})
|
||||
export class ExecuteBatch extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
static cancelled = false;
|
||||
|
||||
static workflowExecutionsProgress: IWorkflowExecutionProgress[][];
|
||||
@@ -58,63 +131,6 @@ export class ExecuteBatch extends BaseCommand {
|
||||
|
||||
static instanceOwner: User;
|
||||
|
||||
static examples = [
|
||||
'$ n8n executeBatch',
|
||||
'$ n8n executeBatch --concurrency=10 --skipList=/data/skipList.json',
|
||||
'$ n8n executeBatch --debug --output=/data/output.json',
|
||||
'$ n8n executeBatch --ids=10,13,15 --shortOutput',
|
||||
'$ n8n executeBatch --snapshot=/data/snapshots --shallow',
|
||||
'$ n8n executeBatch --compare=/data/previousExecutionData --retries=2',
|
||||
];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
debug: Flags.boolean({
|
||||
description: 'Toggles on displaying all errors and debug messages.',
|
||||
}),
|
||||
ids: Flags.string({
|
||||
description:
|
||||
'Specifies workflow IDs to get executed, separated by a comma or a file containing the ids',
|
||||
}),
|
||||
concurrency: Flags.integer({
|
||||
default: 1,
|
||||
description:
|
||||
'How many workflows can run in parallel. Defaults to 1 which means no concurrency.',
|
||||
}),
|
||||
output: Flags.string({
|
||||
description:
|
||||
'Enable execution saving, You must inform an existing folder to save execution via this param',
|
||||
}),
|
||||
snapshot: Flags.string({
|
||||
description:
|
||||
'Enables snapshot saving. You must inform an existing folder to save snapshots via this param.',
|
||||
}),
|
||||
compare: Flags.string({
|
||||
description:
|
||||
'Compares current execution with an existing snapshot. You must inform an existing folder where the snapshots are saved.',
|
||||
}),
|
||||
shallow: Flags.boolean({
|
||||
description:
|
||||
'Compares only if attributes output from node are the same, with no regards to nested JSON objects.',
|
||||
}),
|
||||
|
||||
githubWorkflow: Flags.boolean({
|
||||
description:
|
||||
'Enables more lenient comparison for GitHub workflows. This is useful for reducing false positives when comparing Test workflows.',
|
||||
}),
|
||||
|
||||
skipList: Flags.string({
|
||||
description: 'File containing a comma separated list of workflow IDs to skip.',
|
||||
}),
|
||||
retries: Flags.integer({
|
||||
description: 'Retries failed workflows up to N tries. Default is 1. Set 0 to disable.',
|
||||
default: 1,
|
||||
}),
|
||||
shortOutput: Flags.boolean({
|
||||
description: 'Omits the full execution information from output, displaying only summary.',
|
||||
}),
|
||||
};
|
||||
|
||||
static aliases = ['executeBatch'];
|
||||
|
||||
override needsCommunityPackages = true;
|
||||
@@ -182,7 +198,7 @@ export class ExecuteBatch extends BaseCommand {
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
async run() {
|
||||
const { flags } = await this.parse(ExecuteBatch);
|
||||
const { flags } = this;
|
||||
ExecuteBatch.debug = flags.debug;
|
||||
ExecuteBatch.concurrency = flags.concurrency || 1;
|
||||
|
||||
@@ -345,7 +361,7 @@ export class ExecuteBatch extends BaseCommand {
|
||||
await this.stopProcess(true);
|
||||
|
||||
if (results.summary.failedExecutions > 0) {
|
||||
this.exit(1);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { WorkflowRepository } from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Flags } from '@oclif/core';
|
||||
import type { IWorkflowBase, IWorkflowExecutionDataProcess } from 'n8n-workflow';
|
||||
import { ExecutionBaseError, UnexpectedError, UserError } from 'n8n-workflow';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ActiveExecutions } from '@/active-executions';
|
||||
import { OwnershipService } from '@/services/ownership.service';
|
||||
@@ -12,21 +13,20 @@ import { WorkflowRunner } from '@/workflow-runner';
|
||||
import { BaseCommand } from './base-command';
|
||||
import config from '../config';
|
||||
|
||||
export class Execute extends BaseCommand {
|
||||
static description = '\nExecutes a given workflow';
|
||||
|
||||
static examples = ['$ n8n execute --id=5'];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
id: Flags.string({
|
||||
description: 'id of the workflow to execute',
|
||||
}),
|
||||
rawOutput: Flags.boolean({
|
||||
description: 'Outputs only JSON data, with no other text',
|
||||
}),
|
||||
};
|
||||
const flagsSchema = z.object({
|
||||
id: z.string().describe('id of the workflow to execute').optional(),
|
||||
rawOutput: z.boolean().describe('Outputs only JSON data, with no other text').optional(),
|
||||
/**@deprecated */
|
||||
file: z.string().describe('DEPRECATED: Please use --id instead').optional(),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'execute',
|
||||
description: 'Executes a given workflow',
|
||||
examples: ['--id=5'],
|
||||
flagsSchema,
|
||||
})
|
||||
export class Execute extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
override needsCommunityPackages = true;
|
||||
|
||||
override needsTaskRunner = true;
|
||||
@@ -39,7 +39,7 @@ export class Execute extends BaseCommand {
|
||||
}
|
||||
|
||||
async run() {
|
||||
const { flags } = await this.parse(Execute);
|
||||
const { flags } = this;
|
||||
|
||||
if (!flags.id) {
|
||||
this.logger.info('"--id" has to be set!');
|
||||
|
||||
@@ -1,59 +1,62 @@
|
||||
import type { ICredentialsDb } from '@n8n/db';
|
||||
import { CredentialsRepository } from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Flags } from '@oclif/core';
|
||||
import fs from 'fs';
|
||||
import { Credentials } from 'n8n-core';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import path from 'path';
|
||||
import z from 'zod';
|
||||
|
||||
import type { ICredentialsDecryptedDb } from '@/interfaces';
|
||||
|
||||
import { BaseCommand } from '../base-command';
|
||||
|
||||
export class ExportCredentialsCommand extends BaseCommand {
|
||||
static description = 'Export credentials';
|
||||
|
||||
static examples = [
|
||||
'$ n8n export:credentials --all',
|
||||
'$ n8n export:credentials --id=5 --output=file.json',
|
||||
'$ n8n export:credentials --all --output=backups/latest.json',
|
||||
'$ n8n export:credentials --backup --output=backups/latest/',
|
||||
'$ n8n export:credentials --all --decrypted --output=backups/decrypted.json',
|
||||
];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
all: Flags.boolean({
|
||||
description: 'Export all credentials',
|
||||
}),
|
||||
backup: Flags.boolean({
|
||||
description:
|
||||
'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.',
|
||||
}),
|
||||
id: Flags.string({
|
||||
description: 'The ID of the credential to export',
|
||||
}),
|
||||
output: Flags.string({
|
||||
char: 'o',
|
||||
description: 'Output file name or directory if using separate files',
|
||||
}),
|
||||
pretty: Flags.boolean({
|
||||
description: 'Format the output in an easier to read fashion',
|
||||
}),
|
||||
separate: Flags.boolean({
|
||||
description:
|
||||
'Exports one file per credential (useful for versioning). Must inform a directory via --output.',
|
||||
}),
|
||||
decrypted: Flags.boolean({
|
||||
description:
|
||||
'Exports data decrypted / in plain text. ALL SENSITIVE INFORMATION WILL BE VISIBLE IN THE FILES. Use to migrate from a installation to another that have a different secret key (in the config file).',
|
||||
}),
|
||||
};
|
||||
const flagsSchema = z.object({
|
||||
all: z.boolean().describe('Export all credentials').optional(),
|
||||
backup: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.',
|
||||
)
|
||||
.optional(),
|
||||
id: z.string().describe('The ID of the credential to export').optional(),
|
||||
output: z
|
||||
.string()
|
||||
.alias('o')
|
||||
.describe('Output file name or directory if using separate files')
|
||||
.optional(),
|
||||
pretty: z.boolean().describe('Format the output in an easier to read fashion').optional(),
|
||||
separate: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'Exports one file per credential (useful for versioning). Must inform a directory via --output.',
|
||||
)
|
||||
.optional(),
|
||||
decrypted: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'Exports data decrypted / in plain text. ALL SENSITIVE INFORMATION WILL BE VISIBLE IN THE FILES. Use to migrate from a installation to another that have a different secret key (in the config file).',
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'export:credentials',
|
||||
description: 'Export credentials',
|
||||
examples: [
|
||||
'--all',
|
||||
'--id=5 --output=file.json',
|
||||
'--all --output=backups/latest.json',
|
||||
'--backup --output=backups/latest/',
|
||||
'--all --decrypted --output=backups/decrypted.json',
|
||||
],
|
||||
flagsSchema,
|
||||
})
|
||||
export class ExportCredentialsCommand extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
// eslint-disable-next-line complexity
|
||||
async run() {
|
||||
const { flags } = await this.parse(ExportCredentialsCommand);
|
||||
const { flags } = this;
|
||||
|
||||
if (flags.backup) {
|
||||
flags.all = true;
|
||||
@@ -133,7 +136,7 @@ export class ExportCredentialsCommand extends BaseCommand {
|
||||
for (i = 0; i < credentials.length; i++) {
|
||||
fileContents = JSON.stringify(credentials[i], null, flags.pretty ? 2 : undefined);
|
||||
const filename = `${
|
||||
(flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) +
|
||||
(flags.output!.endsWith(path.sep) ? flags.output : flags.output + path.sep) +
|
||||
credentials[i].id
|
||||
}.json`;
|
||||
fs.writeFileSync(filename, fileContents);
|
||||
|
||||
@@ -1,50 +1,51 @@
|
||||
import { WorkflowRepository } from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Flags } from '@oclif/core';
|
||||
import fs from 'fs';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import path from 'path';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { BaseCommand } from '../base-command';
|
||||
|
||||
export class ExportWorkflowsCommand extends BaseCommand {
|
||||
static description = 'Export workflows';
|
||||
|
||||
static examples = [
|
||||
'$ n8n export:workflow --all',
|
||||
'$ n8n export:workflow --id=5 --output=file.json',
|
||||
'$ n8n export:workflow --all --output=backups/latest/',
|
||||
'$ n8n export:workflow --backup --output=backups/latest/',
|
||||
];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
all: Flags.boolean({
|
||||
description: 'Export all workflows',
|
||||
}),
|
||||
backup: Flags.boolean({
|
||||
description:
|
||||
'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.',
|
||||
}),
|
||||
id: Flags.string({
|
||||
description: 'The ID of the workflow to export',
|
||||
}),
|
||||
output: Flags.string({
|
||||
char: 'o',
|
||||
description: 'Output file name or directory if using separate files',
|
||||
}),
|
||||
pretty: Flags.boolean({
|
||||
description: 'Format the output in an easier to read fashion',
|
||||
}),
|
||||
separate: Flags.boolean({
|
||||
description:
|
||||
'Exports one file per workflow (useful for versioning). Must inform a directory via --output.',
|
||||
}),
|
||||
};
|
||||
const flagsSchema = z.object({
|
||||
all: z.boolean().describe('Export all workflows').optional(),
|
||||
backup: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'Sets --all --pretty --separate for simple backups. Only --output has to be set additionally.',
|
||||
)
|
||||
.optional(),
|
||||
id: z.string().describe('The ID of the workflow to export').optional(),
|
||||
output: z
|
||||
.string()
|
||||
.alias('o')
|
||||
.describe('Output file name or directory if using separate files')
|
||||
.optional(),
|
||||
pretty: z.boolean().describe('Format the output in an easier to read fashion').optional(),
|
||||
separate: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'Exports one file per workflow (useful for versioning). Must inform a directory via --output.',
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'export:workflow',
|
||||
description: 'Export workflows',
|
||||
examples: [
|
||||
'--all',
|
||||
'--id=5 --output=file.json',
|
||||
'--all --output=backups/latest/',
|
||||
'--backup --output=backups/latest/',
|
||||
],
|
||||
flagsSchema,
|
||||
})
|
||||
export class ExportWorkflowsCommand extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
// eslint-disable-next-line complexity
|
||||
async run() {
|
||||
const { flags } = await this.parse(ExportWorkflowsCommand);
|
||||
const { flags } = this;
|
||||
|
||||
if (flags.backup) {
|
||||
flags.all = true;
|
||||
@@ -89,7 +90,7 @@ export class ExportWorkflowsCommand extends BaseCommand {
|
||||
this.logger.error(e.message);
|
||||
this.logger.error(e.stack!);
|
||||
}
|
||||
this.exit(1);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (flags.output) {
|
||||
if (fs.existsSync(flags.output)) {
|
||||
@@ -115,7 +116,7 @@ export class ExportWorkflowsCommand extends BaseCommand {
|
||||
for (i = 0; i < workflows.length; i++) {
|
||||
fileContents = JSON.stringify(workflows[i], null, flags.pretty ? 2 : undefined);
|
||||
const filename = `${
|
||||
(flags.output!.endsWith(path.sep) ? flags.output! : flags.output + path.sep) +
|
||||
(flags.output!.endsWith(path.sep) ? flags.output : flags.output + path.sep) +
|
||||
workflows[i].id
|
||||
}.json`;
|
||||
fs.writeFileSync(filename, fileContents);
|
||||
|
||||
@@ -1,50 +1,56 @@
|
||||
import { CredentialsEntity, Project, User, SharedCredentials, ProjectRepository } from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import type { EntityManager } from '@n8n/typeorm';
|
||||
import { Flags } from '@oclif/core';
|
||||
import glob from 'fast-glob';
|
||||
import fs from 'fs';
|
||||
import { Cipher } from 'n8n-core';
|
||||
import type { ICredentialsEncrypted } from 'n8n-workflow';
|
||||
import { jsonParse, UserError } from 'n8n-workflow';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { UM_FIX_INSTRUCTION } from '@/constants';
|
||||
|
||||
import { BaseCommand } from '../base-command';
|
||||
|
||||
export class ImportCredentialsCommand extends BaseCommand {
|
||||
static description = 'Import credentials';
|
||||
|
||||
static examples = [
|
||||
'$ n8n import:credentials --input=file.json',
|
||||
'$ n8n import:credentials --separate --input=backups/latest/',
|
||||
'$ n8n import:credentials --input=file.json --userId=1d64c3d2-85fe-4a83-a649-e446b07b3aae',
|
||||
'$ n8n import:credentials --input=file.json --projectId=Ox8O54VQrmBrb4qL',
|
||||
'$ n8n import:credentials --separate --input=backups/latest/ --userId=1d64c3d2-85fe-4a83-a649-e446b07b3aae',
|
||||
];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
input: Flags.string({
|
||||
char: 'i',
|
||||
description: 'Input file name or directory if --separate is used',
|
||||
}),
|
||||
separate: Flags.boolean({
|
||||
description: 'Imports *.json files from directory provided by --input',
|
||||
}),
|
||||
userId: Flags.string({
|
||||
description: 'The ID of the user to assign the imported credentials to',
|
||||
}),
|
||||
projectId: Flags.string({
|
||||
description: 'The ID of the project to assign the imported credential to',
|
||||
}),
|
||||
};
|
||||
const flagsSchema = z.object({
|
||||
input: z
|
||||
.string()
|
||||
.alias('i')
|
||||
.describe('Input file name or directory if --separate is used')
|
||||
.optional(),
|
||||
separate: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe('Imports *.json files from directory provided by --input'),
|
||||
userId: z
|
||||
.string()
|
||||
.describe('The ID of the user to assign the imported credentials to')
|
||||
.optional(),
|
||||
projectId: z
|
||||
.string()
|
||||
.describe('The ID of the project to assign the imported credential to')
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'import:credentials',
|
||||
description: 'Import credentials',
|
||||
examples: [
|
||||
'--input=file.json',
|
||||
'--separate --input=backups/latest/',
|
||||
'--input=file.json --userId=1d64c3d2-85fe-4a83-a649-e446b07b3aae',
|
||||
'--input=file.json --projectId=Ox8O54VQrmBrb4qL',
|
||||
'--separate --input=backups/latest/ --userId=1d64c3d2-85fe-4a83-a649-e446b07b3aae',
|
||||
],
|
||||
flagsSchema,
|
||||
})
|
||||
export class ImportCredentialsCommand extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
private transactionManager: EntityManager;
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags } = await this.parse(ImportCredentialsCommand);
|
||||
const { flags } = this;
|
||||
|
||||
if (!flags.input) {
|
||||
this.logger.info('An input file or directory with --input must be provided');
|
||||
|
||||
@@ -6,12 +6,13 @@ import {
|
||||
WorkflowRepository,
|
||||
UserRepository,
|
||||
} from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Flags } from '@oclif/core';
|
||||
import glob from 'fast-glob';
|
||||
import fs from 'fs';
|
||||
import type { IWorkflowBase, WorkflowId } from 'n8n-workflow';
|
||||
import { jsonParse, UserError } from 'n8n-workflow';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { UM_FIX_INSTRUCTION } from '@/constants';
|
||||
import type { IWorkflowToImport } from '@/interfaces';
|
||||
@@ -33,36 +34,38 @@ function assertHasWorkflowsToImport(
|
||||
}
|
||||
}
|
||||
|
||||
export class ImportWorkflowsCommand extends BaseCommand {
|
||||
static description = 'Import workflows';
|
||||
|
||||
static examples = [
|
||||
'$ n8n import:workflow --input=file.json',
|
||||
'$ n8n import:workflow --separate --input=backups/latest/',
|
||||
'$ n8n import:workflow --input=file.json --userId=1d64c3d2-85fe-4a83-a649-e446b07b3aae',
|
||||
'$ n8n import:workflow --input=file.json --projectId=Ox8O54VQrmBrb4qL',
|
||||
'$ n8n import:workflow --separate --input=backups/latest/ --userId=1d64c3d2-85fe-4a83-a649-e446b07b3aae',
|
||||
];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
input: Flags.string({
|
||||
char: 'i',
|
||||
description: 'Input file name or directory if --separate is used',
|
||||
}),
|
||||
separate: Flags.boolean({
|
||||
description: 'Imports *.json files from directory provided by --input',
|
||||
}),
|
||||
userId: Flags.string({
|
||||
description: 'The ID of the user to assign the imported workflows to',
|
||||
}),
|
||||
projectId: Flags.string({
|
||||
description: 'The ID of the project to assign the imported workflows to',
|
||||
}),
|
||||
};
|
||||
const flagsSchema = z.object({
|
||||
input: z
|
||||
.string()
|
||||
.alias('i')
|
||||
.describe('Input file name or directory if --separate is used')
|
||||
.optional(),
|
||||
separate: z
|
||||
.boolean()
|
||||
.describe('Imports *.json files from directory provided by --input')
|
||||
.default(false),
|
||||
userId: z.string().describe('The ID of the user to assign the imported workflows to').optional(),
|
||||
projectId: z
|
||||
.string()
|
||||
.describe('The ID of the project to assign the imported workflows to')
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'import:workflow',
|
||||
description: 'Import workflows',
|
||||
examples: [
|
||||
'--input=file.json',
|
||||
'--separate --input=backups/latest/',
|
||||
'--input=file.json --userId=1d64c3d2-85fe-4a83-a649-e446b07b3aae',
|
||||
'--input=file.json --projectId=Ox8O54VQrmBrb4qL',
|
||||
'--separate --input=backups/latest/ --userId=1d64c3d2-85fe-4a83-a649-e446b07b3aae',
|
||||
],
|
||||
flagsSchema,
|
||||
})
|
||||
export class ImportWorkflowsCommand extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
async run(): Promise<void> {
|
||||
const { flags } = await this.parse(ImportWorkflowsCommand);
|
||||
const { flags } = this;
|
||||
|
||||
if (!flags.input) {
|
||||
this.logger.info('An input file or directory with --input must be provided');
|
||||
|
||||
@@ -9,11 +9,12 @@ import {
|
||||
SharedWorkflowRepository,
|
||||
UserRepository,
|
||||
} from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { In } from '@n8n/typeorm';
|
||||
import { Flags } from '@oclif/core';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { UM_FIX_INSTRUCTION } from '@/constants';
|
||||
import { CredentialsService } from '@/credentials/credentials.service';
|
||||
@@ -24,34 +25,41 @@ import { BaseCommand } from '../base-command';
|
||||
const wrongFlagsError =
|
||||
'You must use exactly one of `--userId`, `--projectId` or `--deleteWorkflowsAndCredentials`.';
|
||||
|
||||
export class Reset extends BaseCommand {
|
||||
static description =
|
||||
'\nResets the database to the default ldap state.\n\nTHIS DELETES ALL LDAP MANAGED USERS.';
|
||||
|
||||
static examples = [
|
||||
'$ n8n ldap:reset --userId=1d64c3d2-85fe-4a83-a649-e446b07b3aae',
|
||||
'$ n8n ldap:reset --projectId=Ox8O54VQrmBrb4qL',
|
||||
'$ n8n ldap:reset --deleteWorkflowsAndCredentials',
|
||||
];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
userId: Flags.string({
|
||||
description:
|
||||
'The ID of the user to assign the workflows and credentials owned by the deleted LDAP users to',
|
||||
}),
|
||||
projectId: Flags.string({
|
||||
description:
|
||||
'The ID of the project to assign the workflows and credentials owned by the deleted LDAP users to',
|
||||
}),
|
||||
deleteWorkflowsAndCredentials: Flags.boolean({
|
||||
description:
|
||||
'Delete all workflows and credentials owned by the users that were created by the users managed via LDAP.',
|
||||
}),
|
||||
};
|
||||
const flagsSchema = z.object({
|
||||
userId: z
|
||||
.string()
|
||||
.describe(
|
||||
'The ID of the user to assign the workflows and credentials owned by the deleted LDAP users to',
|
||||
)
|
||||
.optional(),
|
||||
projectId: z
|
||||
.string()
|
||||
.describe(
|
||||
'The ID of the project to assign the workflows and credentials owned by the deleted LDAP users to',
|
||||
)
|
||||
.optional(),
|
||||
deleteWorkflowsAndCredentials: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'Delete all workflows and credentials owned by the users that were created by the users managed via LDAP.',
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'ldap:reset',
|
||||
description:
|
||||
'Resets the database to the default ldap state.\n\nTHIS DELETES ALL LDAP MANAGED USERS.',
|
||||
examples: [
|
||||
'--userId=1d64c3d2-85fe-4a83-a649-e446b07b3aae',
|
||||
'--projectId=Ox8O54VQrmBrb4qL',
|
||||
'--deleteWorkflowsAndCredentials',
|
||||
],
|
||||
flagsSchema,
|
||||
})
|
||||
export class Reset extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
async run(): Promise<void> {
|
||||
const { flags } = await this.parse(Reset);
|
||||
const { flags } = this;
|
||||
const numberOfOptions =
|
||||
Number(!!flags.userId) +
|
||||
Number(!!flags.projectId) +
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { License } from '@/license';
|
||||
|
||||
import { BaseCommand } from '../base-command';
|
||||
|
||||
@Command({
|
||||
name: 'license:clear',
|
||||
description: 'Clear local license certificate',
|
||||
})
|
||||
export class ClearLicenseCommand extends BaseCommand {
|
||||
static description = 'Clear local license certificate';
|
||||
|
||||
static examples = ['$ n8n license:clear'];
|
||||
|
||||
async run() {
|
||||
this.logger.info('Clearing license from database.');
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { License } from '@/license';
|
||||
|
||||
import { BaseCommand } from '../base-command';
|
||||
|
||||
@Command({
|
||||
name: 'license:info',
|
||||
description: 'Print license information',
|
||||
})
|
||||
export class LicenseInfoCommand extends BaseCommand {
|
||||
static description = 'Print license information';
|
||||
|
||||
static examples = ['$ n8n license:info'];
|
||||
|
||||
async run() {
|
||||
const license = Container.get(License);
|
||||
await license.init({ isCli: true });
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
import { WorkflowRepository } from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Flags } from '@oclif/core';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { BaseCommand } from '../base-command';
|
||||
|
||||
export class ListWorkflowCommand extends BaseCommand {
|
||||
static description = '\nList workflows';
|
||||
|
||||
static examples = [
|
||||
'$ n8n list:workflow',
|
||||
'$ n8n list:workflow --active=true --onlyId',
|
||||
'$ n8n list:workflow --active=false',
|
||||
];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
active: Flags.string({
|
||||
description: 'Filters workflows by active status. Can be true or false',
|
||||
}),
|
||||
onlyId: Flags.boolean({
|
||||
description: 'Outputs workflow IDs only, one per line.',
|
||||
}),
|
||||
};
|
||||
const flagsSchema = z.object({
|
||||
active: z
|
||||
.string()
|
||||
.describe('Filters workflows by active status. Can be true or false')
|
||||
.optional(),
|
||||
onlyId: z.boolean().describe('Outputs workflow IDs only, one per line.').default(false),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'list:workflow',
|
||||
description: 'List workflows',
|
||||
examples: ['', '--active=true --onlyId', '--active=false'],
|
||||
flagsSchema,
|
||||
})
|
||||
export class ListWorkflowCommand extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
async run() {
|
||||
const { flags } = await this.parse(ListWorkflowCommand);
|
||||
const { flags } = this;
|
||||
|
||||
if (flags.active !== undefined && !['true', 'false'].includes(flags.active)) {
|
||||
this.error('The --active flag has to be passed using true or false');
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
import { UserRepository } from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Flags } from '@oclif/core';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { BaseCommand } from '../base-command';
|
||||
|
||||
export class DisableMFACommand extends BaseCommand {
|
||||
static description = 'Disable MFA authentication for a user';
|
||||
|
||||
static examples = ['$ n8n mfa:disable --email=johndoe@example.com'];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
email: Flags.string({
|
||||
description: 'The email of the user to disable the MFA authentication',
|
||||
}),
|
||||
};
|
||||
|
||||
async init() {
|
||||
await super.init();
|
||||
}
|
||||
const flagsSchema = z.object({
|
||||
email: z.string().describe('The email of the user to disable the MFA authentication'),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'mfa:disable',
|
||||
description: 'Disable MFA authentication for a user',
|
||||
examples: ['--email=johndoe@example.com'],
|
||||
flagsSchema,
|
||||
})
|
||||
export class DisableMFACommand extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
async run(): Promise<void> {
|
||||
const { flags } = await this.parse(DisableMFACommand);
|
||||
const { flags } = this;
|
||||
|
||||
if (!flags.email) {
|
||||
this.logger.info('An email with --email must be provided');
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||
import { ExecutionRepository, SettingsRepository } from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Flags } from '@oclif/core';
|
||||
import glob from 'fast-glob';
|
||||
import { createReadStream, createWriteStream, existsSync } from 'fs';
|
||||
import { mkdir } from 'fs/promises';
|
||||
@@ -11,6 +11,7 @@ import { jsonParse, randomString, type IWorkflowExecutionDataProcess } from 'n8n
|
||||
import path from 'path';
|
||||
import replaceStream from 'replacestream';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ActiveExecutions } from '@/active-executions';
|
||||
import { ActiveWorkflowManager } from '@/active-workflow-manager';
|
||||
@@ -36,32 +37,29 @@ import { BaseCommand } from './base-command';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
|
||||
const open = require('open');
|
||||
|
||||
export class Start extends BaseCommand {
|
||||
static description = 'Starts n8n. Makes Web-UI available and starts active workflows';
|
||||
|
||||
static examples = [
|
||||
'$ n8n start',
|
||||
'$ n8n start --tunnel',
|
||||
'$ n8n start -o',
|
||||
'$ n8n start --tunnel -o',
|
||||
];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
open: Flags.boolean({
|
||||
char: 'o',
|
||||
description: 'opens the UI automatically in browser',
|
||||
}),
|
||||
tunnel: Flags.boolean({
|
||||
description:
|
||||
'runs the webhooks via a hooks.n8n.cloud tunnel server. Use only for testing and development!',
|
||||
}),
|
||||
reinstallMissingPackages: Flags.boolean({
|
||||
description:
|
||||
'Attempts to self heal n8n if packages with nodes are missing. Might drastically increase startup times.',
|
||||
}),
|
||||
};
|
||||
const flagsSchema = z.object({
|
||||
open: z.boolean().alias('o').describe('opens the UI automatically in browser').optional(),
|
||||
tunnel: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'runs the webhooks via a hooks.n8n.cloud tunnel server. Use only for testing and development!',
|
||||
)
|
||||
.optional(),
|
||||
reinstallMissingPackages: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'Attempts to self heal n8n if packages with nodes are missing. Might drastically increase startup times.',
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'start',
|
||||
description: 'Starts n8n. Makes Web-UI available and starts active workflows',
|
||||
examples: ['', '--tunnel', '-o', '--tunnel -o'],
|
||||
flagsSchema,
|
||||
})
|
||||
export class Start extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
protected activeWorkflowManager: ActiveWorkflowManager;
|
||||
|
||||
protected server = Container.get(Server);
|
||||
@@ -181,7 +179,7 @@ export class Start extends BaseCommand {
|
||||
scopedLogger.debug(`Host ID: ${this.instanceSettings.hostId}`);
|
||||
}
|
||||
|
||||
const { flags } = await this.parse(Start);
|
||||
const { flags } = this;
|
||||
const { communityPackages } = this.globalConfig.nodes;
|
||||
// cli flag overrides the config env variable
|
||||
if (flags.reinstallMissingPackages) {
|
||||
@@ -272,7 +270,7 @@ export class Start extends BaseCommand {
|
||||
}
|
||||
|
||||
async run() {
|
||||
const { flags } = await this.parse(Start);
|
||||
const { flags } = this;
|
||||
|
||||
// Load settings from database and set them to config.
|
||||
const databaseSettings = await Container.get(SettingsRepository).findBy({
|
||||
|
||||
@@ -1,32 +1,25 @@
|
||||
import { WorkflowRepository } from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Flags } from '@oclif/core';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { BaseCommand } from '../base-command';
|
||||
|
||||
export class UpdateWorkflowCommand extends BaseCommand {
|
||||
static description = 'Update workflows';
|
||||
|
||||
static examples = [
|
||||
'$ n8n update:workflow --all --active=false',
|
||||
'$ n8n update:workflow --id=5 --active=true',
|
||||
];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
active: Flags.string({
|
||||
description: 'Active state the workflow/s should be set to',
|
||||
}),
|
||||
all: Flags.boolean({
|
||||
description: 'Operate on all workflows',
|
||||
}),
|
||||
id: Flags.string({
|
||||
description: 'The ID of the workflow to operate on',
|
||||
}),
|
||||
};
|
||||
const flagsSchema = z.object({
|
||||
active: z.string().describe('Active state the workflow/s should be set to').optional(),
|
||||
all: z.boolean().describe('Operate on all workflows').optional(),
|
||||
id: z.string().describe('The ID of the workflow to operate on').optional(),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'update:workflow',
|
||||
description: 'Update workflows',
|
||||
examples: ['--all --active=false', '--id=5 --active=true'],
|
||||
flagsSchema,
|
||||
})
|
||||
export class UpdateWorkflowCommand extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
async run() {
|
||||
const { flags } = await this.parse(UpdateWorkflowCommand);
|
||||
const { flags } = this;
|
||||
|
||||
if (!flags.all && !flags.id) {
|
||||
this.logger.error('Either option "--all" or "--id" have to be set!');
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SharedWorkflowRepository,
|
||||
UserRepository,
|
||||
} from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { BaseCommand } from '../base-command';
|
||||
@@ -20,11 +21,11 @@ const defaultUserProps = {
|
||||
role: 'global:owner',
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'user-management:reset',
|
||||
description: 'Resets the database to the default user state',
|
||||
})
|
||||
export class Reset extends BaseCommand {
|
||||
static description = 'Resets the database to the default user state';
|
||||
|
||||
static examples = ['$ n8n user-management:reset'];
|
||||
|
||||
async run(): Promise<void> {
|
||||
const owner = await this.getInstanceOwner();
|
||||
const personalProject = await Container.get(ProjectRepository).getPersonalProjectForUserOrFail(
|
||||
@@ -76,6 +77,6 @@ export class Reset extends BaseCommand {
|
||||
async catch(error: Error): Promise<void> {
|
||||
this.logger.error('Error resetting database. See log messages for details.');
|
||||
this.logger.error(error.message);
|
||||
this.exit(1);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { ActiveExecutions } from '@/active-executions';
|
||||
import config from '@/config';
|
||||
@@ -10,15 +10,11 @@ import { WebhookServer } from '@/webhooks/webhook-server';
|
||||
|
||||
import { BaseCommand } from './base-command';
|
||||
|
||||
@Command({
|
||||
name: 'webhook',
|
||||
description: 'Starts n8n webhook process. Intercepts only production URLs.',
|
||||
})
|
||||
export class Webhook extends BaseCommand {
|
||||
static description = 'Starts n8n webhook process. Intercepts only production URLs.';
|
||||
|
||||
static examples = ['$ n8n webhook'];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
};
|
||||
|
||||
protected server = Container.get(WebhookServer);
|
||||
|
||||
override needsCommunityPackages = true;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { inTest } from '@n8n/backend-common';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Flags, type Config } from '@oclif/core';
|
||||
import { z } from 'zod';
|
||||
|
||||
import config from '@/config';
|
||||
import { N8N_VERSION } from '@/constants';
|
||||
@@ -15,28 +16,26 @@ import type { WorkerServerEndpointsConfig } from '@/scaling/worker-server';
|
||||
|
||||
import { BaseCommand } from './base-command';
|
||||
|
||||
export class Worker extends BaseCommand {
|
||||
static description = '\nStarts a n8n worker';
|
||||
|
||||
static examples = ['$ n8n worker --concurrency=5'];
|
||||
|
||||
static flags = {
|
||||
help: Flags.help({ char: 'h' }),
|
||||
concurrency: Flags.integer({
|
||||
default: 10,
|
||||
description: 'How many jobs can run in parallel.',
|
||||
}),
|
||||
};
|
||||
const flagsSchema = z.object({
|
||||
concurrency: z.number().int().default(10).describe('How many jobs can run in parallel.'),
|
||||
});
|
||||
|
||||
@Command({
|
||||
name: 'worker',
|
||||
description: 'Starts a n8n worker',
|
||||
examples: ['--concurrency=5'],
|
||||
flagsSchema,
|
||||
})
|
||||
export class Worker extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
/**
|
||||
* How many jobs this worker may run concurrently.
|
||||
*
|
||||
* Taken from env var `N8N_CONCURRENCY_PRODUCTION_LIMIT` if set to a value
|
||||
* other than -1, else taken from `--concurrency` flag.
|
||||
*/
|
||||
concurrency: number;
|
||||
private concurrency: number;
|
||||
|
||||
scalingService: ScalingService;
|
||||
private scalingService: ScalingService;
|
||||
|
||||
override needsCommunityPackages = true;
|
||||
|
||||
@@ -59,12 +58,12 @@ export class Worker extends BaseCommand {
|
||||
await this.exitSuccessFully();
|
||||
}
|
||||
|
||||
constructor(argv: string[], cmdConfig: Config) {
|
||||
constructor() {
|
||||
if (config.getEnv('executions.mode') !== 'queue') {
|
||||
config.set('executions.mode', 'queue');
|
||||
}
|
||||
|
||||
super(argv, cmdConfig);
|
||||
super();
|
||||
|
||||
this.logger = this.logger.scoped('scaling');
|
||||
}
|
||||
@@ -133,7 +132,7 @@ export class Worker extends BaseCommand {
|
||||
}
|
||||
|
||||
async setConcurrency() {
|
||||
const { flags } = await this.parse(Worker);
|
||||
const { flags } = this;
|
||||
|
||||
const envConcurrency = config.getEnv('executions.concurrency.productionLimit');
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Help } from '@oclif/core';
|
||||
|
||||
// oclif expects a default export
|
||||
// eslint-disable-next-line import-x/no-default-export
|
||||
export default class CustomHelp extends Help {
|
||||
async showRootHelp() {
|
||||
Container.get(Logger).info(
|
||||
'You can find up to date information about the CLI here:\nhttps://docs.n8n.io/hosting/cli-commands/',
|
||||
);
|
||||
}
|
||||
}
|
||||
16
packages/cli/src/zod-alias-support.ts
Normal file
16
packages/cli/src/zod-alias-support.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Monkey-patch zod to support aliases
|
||||
declare module 'zod' {
|
||||
interface ZodType {
|
||||
alias<T extends ZodType>(this: T, aliasName: string): T;
|
||||
}
|
||||
interface ZodTypeDef {
|
||||
_alias: string;
|
||||
}
|
||||
}
|
||||
|
||||
z.ZodType.prototype.alias = function <T extends z.ZodType>(this: T, aliasName: string) {
|
||||
this._def._alias = aliasName;
|
||||
return this;
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { getPersonalProject, mockInstance } from '@n8n/backend-test-utils';
|
||||
import { testDb } from '@n8n/backend-test-utils';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import '@/zod-alias-support';
|
||||
import { ImportCredentialsCommand } from '@/commands/import/credentials';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { setupTestCommand } from '@test-integration/utils/test-command';
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getPersonalProject } from '@n8n/backend-test-utils';
|
||||
import { getAllSharedWorkflows, getAllWorkflows } from '@n8n/backend-test-utils';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import '@/zod-alias-support';
|
||||
import { ImportWorkflowsCommand } from '@/commands/import/workflow';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { setupTestCommand } from '@test-integration/utils/test-command';
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { testDb } from '@n8n/backend-test-utils';
|
||||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import type { Config } from '@oclif/core';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { Class } from 'n8n-core';
|
||||
import type { CommandClass } from '@n8n/decorators';
|
||||
import argvParser from 'yargs-parser';
|
||||
|
||||
import type { BaseCommand } from '@/commands/base-command';
|
||||
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
||||
import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
|
||||
|
||||
mockInstance(MessageEventBus);
|
||||
|
||||
export const setupTestCommand = <T extends BaseCommand>(Command: Class<T>) => {
|
||||
const config = mock<Config>();
|
||||
config.runHook.mockResolvedValue({ successes: [], failures: [] });
|
||||
|
||||
export const setupTestCommand = <T extends CommandClass>(Command: T) => {
|
||||
// mock SIGINT/SIGTERM registration
|
||||
process.once = jest.fn();
|
||||
process.exit = jest.fn() as never;
|
||||
@@ -34,8 +29,9 @@ export const setupTestCommand = <T extends BaseCommand>(Command: Class<T>) => {
|
||||
});
|
||||
|
||||
const run = async (argv: string[] = []) => {
|
||||
const command = new Command(argv, config);
|
||||
await command.init();
|
||||
const command = new Command();
|
||||
command.flags = argvParser(argv);
|
||||
await command.init?.();
|
||||
await command.run();
|
||||
return command;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { inTest, Logger } from '@n8n/backend-common';
|
||||
import type { InstanceType } from '@n8n/constants';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { NodeOptions } from '@sentry/node';
|
||||
@@ -81,6 +81,8 @@ export class ErrorReporter {
|
||||
serverName,
|
||||
releaseDate,
|
||||
}: ErrorReporterInitOptions) {
|
||||
if (inTest) return;
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
this.error(error);
|
||||
});
|
||||
|
||||
@@ -181,7 +181,7 @@ export class InstanceSettings {
|
||||
errorMessage: `Error parsing n8n-config file "${this.settingsFile}". It does not seem to be valid JSON.`,
|
||||
});
|
||||
|
||||
if (!inTest) this.logger.info(`User settings loaded from: ${this.settingsFile}`);
|
||||
if (!inTest) this.logger.debug(`User settings loaded from: ${this.settingsFile}`);
|
||||
|
||||
const { encryptionKey, tunnelSubdomain } = settings;
|
||||
|
||||
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -1274,9 +1274,6 @@ importers:
|
||||
'@n8n_io/license-sdk':
|
||||
specifier: 2.22.0
|
||||
version: 2.22.0
|
||||
'@oclif/core':
|
||||
specifier: 4.0.7
|
||||
version: 4.0.7
|
||||
'@rudderstack/rudder-sdk-node':
|
||||
specifier: 2.1.4
|
||||
version: 2.1.4(tslib@2.8.1)
|
||||
@@ -1496,6 +1493,9 @@ importers:
|
||||
yamljs:
|
||||
specifier: 0.3.0
|
||||
version: 0.3.0
|
||||
yargs-parser:
|
||||
specifier: 21.1.1
|
||||
version: 21.1.1
|
||||
zod:
|
||||
specifier: 3.25.67
|
||||
version: 3.25.67
|
||||
@@ -1575,6 +1575,9 @@ importers:
|
||||
'@types/yamljs':
|
||||
specifier: ^0.2.31
|
||||
version: 0.2.31
|
||||
'@types/yargs-parser':
|
||||
specifier: 21.0.0
|
||||
version: 21.0.0
|
||||
'@vvo/tzdb':
|
||||
specifier: ^6.141.0
|
||||
version: 6.141.0
|
||||
|
||||
Reference in New Issue
Block a user