mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +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';
|
||||
|
||||
Reference in New Issue
Block a user