UNPKG

@darksnow-ui/commander

Version:

Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs

945 lines (769 loc) 26.6 kB
# Proposal: Commander Middleware System > Sistema de middleware/interceptor para `@darksnow-ui/commander` com suporte a filtros granulares. ## Visão Geral Adicionar sistema de middleware inspirado em Express/Koa/Fastify que permite: 1. **Interceptar** comandos antes/depois da execução 2. **Cancelar** execução baseado em condições 3. **Modificar** input antes de executar 4. **Compor** middlewares reutilizáveis (logging, auth, debug, etc.) 5. **Filtrar** por comando, categoria, pattern ou função customizada --- ## API Proposta ### Registro de Middlewares ```typescript // ============================================================================ // GLOBAL - roda em TODOS os comandos // ============================================================================ commander.use(loggingMiddleware) commander.use([analyticsMiddleware, errorTrackingMiddleware]) // ============================================================================ // POR CATEGORIA // ============================================================================ commander.use("file", authMiddleware) commander.use("admin", [authMiddleware, roleMiddleware("admin")]) // ============================================================================ // POR COMANDO ESPECÍFICO // ============================================================================ commander.use("user:delete", confirmMiddleware) commander.use("user:delete", [authMiddleware, confirmMiddleware, auditMiddleware]) // ============================================================================ // POR PATTERN (glob-like) // ============================================================================ commander.use("file:*", auditMiddleware) // file:save, file:delete, etc commander.use("admin:*", adminOnlyMiddleware) // admin:users, admin:settings commander.use("api:v2:*", rateLimitMiddleware) // api:v2:users, api:v2:products // ============================================================================ // POR FILTRO CUSTOMIZADO // ============================================================================ commander.use( (cmd) => cmd.tags?.includes("dangerous"), confirmMiddleware ) commander.use( (cmd) => cmd.category === "file" && cmd.owner === "editor", [validateMiddleware, auditMiddleware] ) // ============================================================================ // MÚLTIPLOS MATCHERS // ============================================================================ commander.use(["file:*", "edit:*"], loggingMiddleware) commander.use( ["user:delete", "data:purge", "system:reset"], [authMiddleware, confirmMiddleware] ) // ============================================================================ // REMOÇÃO // ============================================================================ commander.unuse(loggingMiddleware) // remove de todos os registros commander.unuse("file:*", auditMiddleware) // remove só desse matcher commander.clearMiddlewares() // remove todos commander.clearMiddlewares("admin:*") // remove todos de um matcher ``` --- ## Tipos ```typescript // ============================================================================ // MIDDLEWARE CONTEXT // ============================================================================ export interface MiddlewareContext<TInput = any, TOutput = any> { /** O comando sendo executado */ readonly command: Command<TInput, TOutput> /** Input original (imutável) */ readonly input: TInput /** Input modificado (pode ser alterado pelo middleware) */ modifiedInput?: TInput /** Fonte da execução */ readonly source: "palette" | "shortcut" | "api" /** Timestamp do início */ readonly startTime: Date /** Metadados extensíveis (middlewares podem adicionar dados) */ meta: Record<string, any> /** Referência ao Commander */ readonly commander: Commander } // ============================================================================ // MIDDLEWARE FUNCTION // ============================================================================ export type NextFunction<TOutput = any> = () => Promise<TOutput> export type Middleware<TInput = any, TOutput = any> = ( ctx: MiddlewareContext<TInput, TOutput>, next: NextFunction<TOutput> ) => Promise<TOutput | MiddlewareCancelResult> // ============================================================================ // CANCEL RESULT // ============================================================================ export interface MiddlewareCancelResult { readonly cancelled: true readonly reason?: string readonly data?: any } export function isCancelled(result: any): result is MiddlewareCancelResult { return result && typeof result === "object" && result.cancelled === true } // ============================================================================ // MATCHER TYPES // ============================================================================ export type MiddlewareMatcher = | "*" // global | CommandCategory // "file", "edit", "admin" | CommandKey // "file:save" | `${string}:*` // pattern "file:*" | ((command: Command) => boolean) // função customizada export type MatcherInput = MiddlewareMatcher | MiddlewareMatcher[] export type MiddlewareInput = Middleware | Middleware[] // ============================================================================ // INTERNAL ENTRY // ============================================================================ interface MiddlewareEntry { id: string matcher: MiddlewareMatcher middleware: Middleware priority: number // para ordenação } ``` --- ## Ordem de Execução Quando `invoke("file:save", input)` é chamado: ``` ┌─────────────────────────────────────────────────────────────────┐ │ MIDDLEWARE PIPELINE │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 1. GLOBAL middlewares (ordem de registro) │ │ └── loggingMiddleware │ │ └── analyticsMiddleware │ │ │ │ 2. CATEGORY middlewares ("file") │ │ └── authMiddleware │ │ │ │ 3. PATTERN middlewares ("file:*") │ │ └── auditMiddleware │ │ │ │ 4. SPECIFIC middlewares ("file:save") │ │ └── validateMiddleware │ │ │ │ 5. CUSTOM FILTER middlewares (que matcham) │ │ └── (middlewares cujo filtro retorna true) │ │ │ │ 6. COMMAND HANDLER │ │ └── command.handle(finalInput) │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` **Prioridade de matching (do mais genérico ao mais específico):** 1. `"*"` (global) 2. `CommandCategory` ("file", "admin") 3. `Pattern` ("file:*") 4. `CommandKey` ("file:save") 5. `Function` (filtros customizados) Dentro de cada grupo, executa na **ordem de registro**. --- ## Implementação ### Commander Class (modificações) ```typescript export default class Commander { // ... propriedades existentes ... /** Middleware entries */ private middlewareEntries: MiddlewareEntry[] = [] private middlewareIdCounter = 0 // ========================================================================== // MIDDLEWARE REGISTRATION // ========================================================================== /** * Registra middleware(s) com matcher opcional */ use(middleware: MiddlewareInput): this use(matcher: MatcherInput, middleware: MiddlewareInput): this use( matcherOrMiddleware: MatcherInput | MiddlewareInput, middleware?: MiddlewareInput ): this { const matchers = middleware ? this.toArray(matcherOrMiddleware as MatcherInput) : ["*" as const] const middlewares = middleware ? this.toArray(middleware) : this.toArray(matcherOrMiddleware as MiddlewareInput) for (const matcher of matchers) { for (const mw of middlewares) { this.middlewareEntries.push({ id: `mw_${++this.middlewareIdCounter}`, matcher, middleware: mw, priority: this.getMatcherPriority(matcher), }) } } this.emit("middleware:added", { matchers, middlewares }) return this } /** * Remove middleware(s) */ unuse(middleware: Middleware): this unuse(matcher: MatcherInput, middleware: Middleware): this unuse( matcherOrMiddleware: MatcherInput | Middleware, middleware?: Middleware ): this { if (middleware) { // Remove de matchers específicos const matchers = this.toArray(matcherOrMiddleware as MatcherInput) this.middlewareEntries = this.middlewareEntries.filter( (entry) => entry.middleware !== middleware || !matchers.some((m) => this.matchersEqual(m, entry.matcher)) ) } else { // Remove de todos os registros const mw = matcherOrMiddleware as Middleware this.middlewareEntries = this.middlewareEntries.filter( (entry) => entry.middleware !== mw ) } return this } /** * Remove todos os middlewares (opcionalmente de um matcher específico) */ clearMiddlewares(matcher?: MatcherInput): this { if (matcher) { const matchers = this.toArray(matcher) this.middlewareEntries = this.middlewareEntries.filter( (entry) => !matchers.some((m) => this.matchersEqual(m, entry.matcher)) ) } else { this.middlewareEntries = [] } return this } /** * Retorna middlewares registrados */ getMiddlewares(matcher?: MiddlewareMatcher): MiddlewareEntry[] { if (matcher) { return this.middlewareEntries.filter((e) => this.matchersEqual(matcher, e.matcher) ) } return [...this.middlewareEntries] } // ========================================================================== // MIDDLEWARE MATCHING // ========================================================================== /** * Retorna prioridade do matcher (menor = executa primeiro) */ private getMatcherPriority(matcher: MiddlewareMatcher): number { if (matcher === "*") return 0 if (typeof matcher === "function") return 50 if (matcher.endsWith(":*")) return 20 if (["system", "file", "edit", "view", "debug", "tools", "custom"].includes(matcher)) return 10 return 30 // comando específico } /** * Verifica se comando casa com matcher */ private matchesCommand(matcher: MiddlewareMatcher, command: Command): boolean { if (matcher === "*") return true if (typeof matcher === "function") { try { return matcher(command) } catch { return false } } // Pattern: "file:*" if (matcher.endsWith(":*")) { const prefix = matcher.slice(0, -1) // "file:" return command.key.startsWith(prefix) } // Category match if (command.category === matcher) return true // Exact key match return command.key === matcher } /** * Compara dois matchers */ private matchersEqual(a: MiddlewareMatcher, b: MiddlewareMatcher): boolean { if (typeof a === "function" || typeof b === "function") { return a === b // referência } return a === b } /** * Coleta middlewares que se aplicam ao comando */ private getMiddlewaresForCommand(command: Command): Middleware[] { return this.middlewareEntries .filter((entry) => this.matchesCommand(entry.matcher, command)) .sort((a, b) => a.priority - b.priority) .map((entry) => entry.middleware) } // ========================================================================== // EXECUTION WITH MIDDLEWARE // ========================================================================== async invoke<T = any>( key: CommandKey, input?: any, source: "palette" | "shortcut" | "api" = "api" ): Promise<T> { const command = this._commands.get(key) if (!command) { const error = new CommandNotFoundError(key) this.emit("command:error", key, error) throw error } // Build context const ctx: MiddlewareContext = { command, input, source, startTime: new Date(), meta: {}, commander: this, } this.emit("command:executing", ctx) try { // Get applicable middlewares const middlewares = this.getMiddlewaresForCommand(command) // Execute through pipeline const result = await this.executePipeline<T>(ctx, middlewares) // Handle cancellation if (isCancelled(result)) { this.emit("command:cancelled", ctx, result) return result as T } // Track successful execution this.trackExecution(ctx, result) this.addToRecent(key) this.emit("command:completed", ctx, result) return result } catch (error) { const executionError = error instanceof Error ? new CommandExecutionError(key, error) : new CommandExecutionError(key, new Error(String(error))) this.emit("command:failed", ctx, executionError) throw executionError } } /** * Executa o pipeline de middlewares (Koa-style compose) */ private async executePipeline<T>( ctx: MiddlewareContext, middlewares: Middleware[] ): Promise<T> { let index = 0 const dispatch = async (): Promise<T> => { if (index < middlewares.length) { const middleware = middlewares[index++] return middleware(ctx, dispatch) as Promise<T> } // End of pipeline: execute command return this.executeCommand<T>(ctx) } return dispatch() } /** * Execução real do comando (após middlewares) */ private async executeCommand<T>(ctx: MiddlewareContext): Promise<T> { const { command } = ctx // Availability check if (!(await this.isCommandAvailable(command))) { throw new CommandUnavailableError(command.key) } // Use modified input if available const finalInput = ctx.modifiedInput ?? ctx.input // Execute with timeout return this.executeWithTimeout<T>(command, finalInput) } // ========================================================================== // HELPERS // ========================================================================== private toArray<T>(value: T | T[]): T[] { return Array.isArray(value) ? value : [value] } } ``` --- ## Helpers de Middleware ```typescript // src/middleware/helpers.ts import type { Middleware, MiddlewareContext, MiddlewareCancelResult } from "../types" /** * Cria middleware que executa ANTES do comando */ export function before<TInput = any>( fn: (ctx: MiddlewareContext<TInput>) => Promise<void | { input?: TInput cancel?: boolean reason?: string }> ): Middleware<TInput> { return async (ctx, next) => { const result = await fn(ctx) if (result?.cancel) { return { cancelled: true, reason: result.reason } as MiddlewareCancelResult } if (result?.input !== undefined) { ctx.modifiedInput = result.input } return next() } } /** * Cria middleware que executa DEPOIS do comando */ export function after<TInput = any, TOutput = any>( fn: (ctx: MiddlewareContext<TInput, TOutput>, result: TOutput) => Promise<TOutput | void> ): Middleware<TInput, TOutput> { return async (ctx, next) => { const result = await next() const modified = await fn(ctx, result) return modified ?? result } } /** * Cria middleware condicional */ export function when( condition: (ctx: MiddlewareContext) => boolean | Promise<boolean>, middleware: Middleware ): Middleware { return async (ctx, next) => { if (await condition(ctx)) { return middleware(ctx, next) } return next() } } /** * Compõe múltiplos middlewares em um */ export function compose(...middlewares: Middleware[]): Middleware { return async (ctx, next) => { let index = 0 const dispatch = async (): Promise<any> => { if (index < middlewares.length) { const mw = middlewares[index++] return mw(ctx, dispatch) } return next() } return dispatch() } } /** * Middleware de logging */ export function logging( logger: (msg: string, data?: any) => void = console.log ): Middleware { return async (ctx, next) => { const start = Date.now() logger(`[Commander] → ${ctx.command.key}`, { input: ctx.input, source: ctx.source }) try { const result = await next() logger(`[Commander] ✓ ${ctx.command.key} (${Date.now() - start}ms)`) return result } catch (error) { logger(`[Commander] ✗ ${ctx.command.key} (${Date.now() - start}ms)`, { error }) throw error } } } /** * Middleware de analytics */ export function analytics( tracker: (event: string, data: Record<string, any>) => void ): Middleware { return async (ctx, next) => { const start = Date.now() try { const result = await next() tracker("command:success", { key: ctx.command.key, category: ctx.command.category, duration: Date.now() - start, source: ctx.source, }) return result } catch (error) { tracker("command:error", { key: ctx.command.key, category: ctx.command.category, duration: Date.now() - start, source: ctx.source, error: error instanceof Error ? error.message : String(error), }) throw error } } } /** * Middleware de rate limiting */ export function rateLimit(options: { maxPerSecond?: number maxPerMinute?: number keyFn?: (ctx: MiddlewareContext) => string }): Middleware { const { maxPerSecond = 10, maxPerMinute = 100, keyFn = (ctx) => ctx.command.key } = options const counters = new Map<string, { second: number[]; minute: number[] }>() return async (ctx, next) => { const key = keyFn(ctx) const now = Date.now() if (!counters.has(key)) { counters.set(key, { second: [], minute: [] }) } const counter = counters.get(key)! // Clean old entries counter.second = counter.second.filter((t) => now - t < 1000) counter.minute = counter.minute.filter((t) => now - t < 60000) // Check limits if (counter.second.length >= maxPerSecond) { return { cancelled: true, reason: "rate_limit_second" } as MiddlewareCancelResult } if (counter.minute.length >= maxPerMinute) { return { cancelled: true, reason: "rate_limit_minute" } as MiddlewareCancelResult } // Track counter.second.push(now) counter.minute.push(now) return next() } } /** * Middleware de confirmação */ export function confirm( promptFn: (ctx: MiddlewareContext) => Promise<boolean> ): Middleware { return async (ctx, next) => { const confirmed = await promptFn(ctx) if (!confirmed) { return { cancelled: true, reason: "user_cancelled" } as MiddlewareCancelResult } return next() } } /** * Middleware de autenticação */ export function auth( checkFn: () => boolean | Promise<boolean>, onUnauthorized?: (ctx: MiddlewareContext) => void ): Middleware { return async (ctx, next) => { const isAuthenticated = await checkFn() if (!isAuthenticated) { onUnauthorized?.(ctx) return { cancelled: true, reason: "unauthorized" } as MiddlewareCancelResult } return next() } } /** * Middleware de role/permission */ export function requireRole( role: string | string[], getRoles: () => string[] | Promise<string[]> ): Middleware { const requiredRoles = Array.isArray(role) ? role : [role] return async (ctx, next) => { const userRoles = await getRoles() const hasRole = requiredRoles.some((r) => userRoles.includes(r)) if (!hasRole) { return { cancelled: true, reason: "forbidden", data: { requiredRoles } } as MiddlewareCancelResult } return next() } } ``` --- ## Exemplos de Uso ### 1. Setup Básico ```typescript import { Commander } from "@darksnow-ui/commander" import { logging, analytics, auth, confirm } from "@darksnow-ui/commander/middleware" const commander = new Commander() // Global: logging em tudo commander.use(logging()) // Global: analytics commander.use(analytics((event, data) => { mixpanel.track(event, data) })) ``` ### 2. Autenticação por Categoria ```typescript // Comandos de file precisam de auth commander.use("file", auth( () => !!currentUser, () => router.push("/login") )) // Comandos admin precisam de role commander.use("admin:*", [ auth(() => !!currentUser), requireRole("admin", () => currentUser?.roles ?? []) ]) ``` ### 3. Confirmação para Ações Perigosas ```typescript commander.use( ["user:delete", "data:purge", "system:reset"], confirm(async (ctx) => { return await showConfirmDialog({ title: "Confirmar ação", message: `Deseja executar "${ctx.command.label}"?`, confirmText: "Sim, executar", cancelText: "Cancelar" }) }) ) ``` ### 4. Debug Mode ```typescript let debugEnabled = false const debugMiddleware: Middleware = async (ctx, next) => { if (!debugEnabled) return next() if (ctx.command.key === "debug:toggle") return next() const action = await showDebugModal({ command: ctx.command, input: ctx.input, }) if (action.cancel) { return { cancelled: true, reason: "debug_cancelled" } } if (action.modifiedInput !== undefined) { ctx.modifiedInput = action.modifiedInput } return next() } commander.use(debugMiddleware) // Comando para toggle commander.add({ key: "debug:toggle", label: "Toggle Debug Mode", shortcut: "alt+shift+d", handle: async () => { debugEnabled = !debugEnabled return { enabled: debugEnabled } } }) ``` ### 5. Rate Limiting em APIs ```typescript commander.use( "api:*", rateLimit({ maxPerSecond: 5, maxPerMinute: 100, }) ) ``` ### 6. Middleware Condicional ```typescript import { when } from "@darksnow-ui/commander/middleware" // Só loga comandos em dev commander.use( when( () => process.env.NODE_ENV === "development", logging() ) ) // Confirma só comandos marcados como dangerous commander.use( (cmd) => cmd.tags?.includes("dangerous"), confirm(async (ctx) => { return await showDangerConfirm(ctx.command.label) }) ) ``` --- ## React Hook: useMiddleware ```typescript // Hook para adicionar middleware temporário (cleanup no unmount) export function useMiddleware( matcher: MatcherInput | MiddlewareInput, middleware?: MiddlewareInput ) { const { commander } = useCommander() useEffect(() => { if (middleware) { commander.use(matcher as MatcherInput, middleware) } else { commander.use(matcher as MiddlewareInput) } return () => { if (middleware) { const middlewares = Array.isArray(middleware) ? middleware : [middleware] middlewares.forEach((mw) => commander.unuse(matcher as MatcherInput, mw)) } else { const middlewares = Array.isArray(matcher) ? matcher : [matcher as Middleware] middlewares.forEach((mw) => commander.unuse(mw)) } } }, [commander]) // Sem deps para evitar re-registro } // Uso function ProtectedArea() { useMiddleware("admin:*", authMiddleware) return <AdminPanel /> } ``` --- ## Breaking Changes **Nenhum!** A API existente continua funcionando: - `invoke()` mantém mesma assinatura - Eventos continuam sendo emitidos - `attempt()` funciona igual - Sem middlewares registrados = comportamento idêntico ao atual --- ## Checklist de Implementação - [ ] Adicionar tipos de middleware em `types.ts` - [ ] Adicionar `MiddlewareEntry` e propriedades no Commander - [ ] Implementar `use()` com overloads - [ ] Implementar `unuse()` com overloads - [ ] Implementar `clearMiddlewares()` - [ ] Implementar `getMiddlewares()` - [ ] Implementar `getMatcherPriority()` - [ ] Implementar `matchesCommand()` - [ ] Implementar `getMiddlewaresForCommand()` - [ ] Refatorar `invoke()` para usar pipeline - [ ] Implementar `executePipeline()` - [ ] Criar `src/middleware/helpers.ts` - [ ] Criar `src/middleware/index.ts` (exports) - [ ] Adicionar evento `command:cancelled` - [ ] Testes unitários para middleware system - [ ] Testes de integração - [ ] Hook `useMiddleware` - [ ] Documentação - [ ] Exemplos --- ## Estrutura de Arquivos ``` src/ ├── commander.ts # Modificado com middleware system ├── types.ts # Novos tipos de middleware ├── middleware/ │ ├── index.ts # Exports públicos │ ├── helpers.ts # before, after, when, compose │ ├── logging.ts # logging middleware │ ├── analytics.ts # analytics middleware │ ├── auth.ts # auth, requireRole │ ├── rateLimit.ts # rate limiting │ └── confirm.ts # confirmation dialogs ├── hooks/ │ ├── ...existing... │ └── useMiddleware.ts # Novo hook └── index.ts # Atualizar exports ``` --- *Atualizado em: 2025-12-03* *Autor: Anderson / Claude*