UNPKG

typebus-cqrs

Version:

Simple, type-safe CQRS library with auto-registration and minimal boilerplate (typebus-cqrs)

842 lines (834 loc) 31.1 kB
/** * Factory class for creating message instances. * Handles the creation of commands, queries, and events with proper typing. */ class MessageFactory { /** * Creates a command message. * @template TCommandMap - Command map type * @template T - Command type key * @param {T} type - The command type. * @param {CommandData<TCommandMap, T>} data - The command data. * @param {string} aggregateId - The aggregate ID. * @param {Record<string, any>} [metadata] - Optional metadata. * @returns {ICommand<TCommandMap, T>} The created command. */ createCommand(type, data, aggregateId, metadata) { return { id: this.generateId(), type, timestamp: new Date(), data, aggregateId, metadata }; } /** * Creates a query message. * @template TQueryMap - Query map type * @template T - Query type key * @param {T} type - The query type. * @param {QueryParams<TQueryMap, T>} params - The query parameters. * @param {Record<string, any>} [metadata] - Optional metadata. * @returns {IQuery<TQueryMap, T>} The created query. */ createQuery(type, params, metadata) { return { id: this.generateId(), type, timestamp: new Date(), params, metadata }; } /** * Creates an event message. * @template TEventMap - Event map type * @template T - Event type key * @param {T} type - The event type. * @param {EventData<TEventMap, T>} data - The event data. * @param {string} aggregateId - The aggregate ID. * @param {number} version - The event version. * @param {Record<string, any>} [metadata] - Optional metadata. * @returns {IEvent<TEventMap, T>} The created event. */ createEvent(type, data, aggregateId, version, metadata) { return { id: this.generateId(), type, timestamp: new Date(), data, aggregateId, version, metadata }; } /** * Generates a unique message ID. * @returns {string} A unique message ID. */ generateId() { return `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } } /** * Main class of the TypeBus-CQRS library. Implements the IMessageBus interface. * Handles registration and execution of commands, queries, and events with middleware support. * @template TCommandMap - Command map type * @template TQueryMap - Query map type * @template TEventMap - Event map type * @implements {IMessageBus<TCommandMap, TQueryMap, TEventMap>} */ class TypeBus { /** * Creates a new TypeBus instance. * @param {TypeBusConfig} [config] */ constructor(config = {}) { this.commandHandlers = new Map(); this.queryHandlers = new Map(); this.eventHandlers = new Map(); this.middlewares = []; this.messageFactory = new MessageFactory(); this.config = { enableMetrics: true, enableLogging: true, logLevel: 'info', maxMiddleware: 10, commandTimeout: 30000, queryTimeout: 10000, ...config }; } // ================================================================================ // Middleware Management // ================================================================================ /** * Registers a middleware to the bus. * @param {IMiddleware} middleware */ use(middleware) { if (this.middlewares.length >= this.config.maxMiddleware) { throw new Error(`Maximum number of middleware (${this.config.maxMiddleware}) exceeded`); } this.middlewares.push(middleware); } // ================================================================================ // Handler Registration // ================================================================================ /** * Registers a command handler for a specific command type. * @template T * @param {T} commandType * @param {IMessageHandler<any, CommandResult<TCommandMap, T>>} handler */ registerCommandHandler(commandType, handler) { if (this.commandHandlers.has(commandType)) { throw new Error(`Command handler for '${commandType}' already registered`); } this.commandHandlers.set(commandType, handler); if (this.config.enableLogging && this.config.logLevel === 'debug') { console.log(`📝 Registered command handler: ${commandType}`); } } /** * Registers a query handler for a specific query type. * @template T * @param {T} queryType * @param {IMessageHandler<any, QueryResult<TQueryMap, T>>} handler */ registerQueryHandler(queryType, handler) { if (this.queryHandlers.has(queryType)) { throw new Error(`Query handler for '${queryType}' already registered`); } this.queryHandlers.set(queryType, handler); if (this.config.enableLogging && this.config.logLevel === 'debug') { console.log(`📖 Registered query handler: ${queryType}`); } } /** * Registers an event handler for a specific event type. * @template T * @param {T} eventType * @param {IMessageHandler<any, void>} handler */ registerEventHandler(eventType, handler) { if (!this.eventHandlers.has(eventType)) { this.eventHandlers.set(eventType, []); } this.eventHandlers.get(eventType).push(handler); if (this.config.enableLogging && this.config.logLevel === 'debug') { const count = this.eventHandlers.get(eventType).length; console.log(`📢 Registered event handler: ${eventType} (${count} total)`); } } // ================================================================================ // Message Execution // ================================================================================ /** * Executes a command message. * @template T * @param {T} type * @param {CommandData<TCommandMap, T>} data * @param {string} aggregateId * @param {Record<string, any>} [metadata] * @returns {Promise<CommandResult<TCommandMap, T>>} */ async executeCommand(type, data, aggregateId, metadata) { const command = this.messageFactory.createCommand(type, data, aggregateId, metadata); const handler = this.commandHandlers.get(type); if (!handler) { throw new Error(`No handler registered for command: ${type}`); } return await this.executeWithMiddleware(command, handler, this.config.commandTimeout); } /** * Executes a query message. * @template T * @param {T} type * @param {QueryParams<TQueryMap, T>} params * @param {Record<string, any>} [metadata] * @returns {Promise<QueryResult<TQueryMap, T>>} */ async executeQuery(type, params, metadata) { const query = this.messageFactory.createQuery(type, params, metadata); const handler = this.queryHandlers.get(type); if (!handler) { throw new Error(`No handler registered for query: ${type}`); } return await this.executeWithMiddleware(query, handler, this.config.queryTimeout); } /** * Publishes an event message to all registered handlers. * @template T * @param {T} type * @param {EventData<TEventMap, T>} data * @param {string} aggregateId * @param {number} version * @param {Record<string, any>} [metadata] * @returns {Promise<void>} */ async publishEvent(type, data, aggregateId, version, metadata) { const event = this.messageFactory.createEvent(type, data, aggregateId, version, metadata); const handlers = this.eventHandlers.get(type) || []; if (handlers.length === 0) { if (this.config.enableLogging && this.config.logLevel === 'debug') { console.log(`📢 No handlers registered for event: ${type}`); } return; } // Execute all handlers in parallel const promises = handlers.map(handler => this.executeWithMiddleware(event, handler, this.config.commandTimeout)); await Promise.all(promises); } // ================================================================================ // Private Methods // ================================================================================ /** * Executes a message through the middleware pipeline. * @template T, R * @param {T} message * @param {IMessageHandler<T, R>} handler * @param {number} timeout * @returns {Promise<R>} */ async executeWithMiddleware(message, handler, timeout) { // Create middleware chain const dispatch = async (msg) => { return await handler.handle(msg); }; // Apply middleware in reverse order let chain = dispatch; for (let i = this.middlewares.length - 1; i >= 0; i--) { const middleware = this.middlewares[i]; const next = chain; chain = async (msg) => middleware.execute(msg, next); } return await this.withTimeout(chain(message), timeout, message.type); } /** * Executes a promise with a timeout. * @template T * @param {Promise<T>} promise * @param {number} timeoutMs * @param {string} operationType * @returns {Promise<T>} */ async withTimeout(promise, timeoutMs, operationType) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error(`${operationType} timed out after ${timeoutMs}ms`)); }, timeoutMs); }); return Promise.race([promise, timeoutPromise]); } // ================================================================================ // Utility Methods // ================================================================================ /** * Clears all registered handlers and middleware. */ clear() { this.commandHandlers.clear(); this.queryHandlers.clear(); this.eventHandlers.clear(); this.middlewares = []; if (this.config.enableLogging) { console.log('🧹 TypeBus cleared all handlers and middleware'); } } /** * Gets statistics about the bus. * @returns {object} */ getStats() { return { commandHandlers: this.commandHandlers.size, queryHandlers: this.queryHandlers.size, eventHandlers: Array.from(this.eventHandlers.values()).reduce((sum, handlers) => sum + handlers.length, 0), middleware: this.middlewares.length, totalHandlers: this.commandHandlers.size + this.queryHandlers.size + Array.from(this.eventHandlers.values()).reduce((sum, handlers) => sum + handlers.length, 0) }; } /** * Gets all registered handlers. * @returns {object} */ getRegisteredHandlers() { return { commands: Array.from(this.commandHandlers.keys()), queries: Array.from(this.queryHandlers.keys()), events: Array.from(this.eventHandlers.keys()) }; } } /** ================================================================================ * Builder for creating and registering command handlers with TypeBus-CQRS. ================================================================================ */ class TypedCommandBuilder { /** * Creates and registers a command handler. * @template TCommandMap - Command map type * @template T - Command type key * @param {TypeBus<TCommandMap, any, any>} bus - The TypeBus-CQRS instance. * @param {T} commandType - The command type. * @param {(data: CommandData<TCommandMap, T>, aggregateId: string, metadata?: Record<string, any>) => Promise<CommandResult<TCommandMap, T>>} handlerLogic - The handler logic. * @returns {object} Command executor and handler meta. */ static create(bus, commandType, handlerLogic) { const handler = { async handle(command) { return await handlerLogic(command.data, command.aggregateId, command.metadata); } }; bus.registerCommandHandler(commandType, handler); return { async execute(data, aggregateId, metadata) { return await bus.executeCommand(commandType, data, aggregateId, metadata); }, type: commandType, handler }; } } /** ================================================================================ * Builder for creating and registering query handlers with TypeBus-CQRS. ================================================================================ */ class TypedQueryBuilder { /** * Creates and registers a query handler. * @template TQueryMap - Query map type * @template T - Query type key * @param {TypeBus<any, TQueryMap, any>} bus - The TypeBus-CQRS instance. * @param {T} queryType - The query type. * @param {(params: QueryParams<TQueryMap, T>, metadata?: Record<string, any>) => Promise<QueryResult<TQueryMap, T>>} handlerLogic - The handler logic. * @returns {object} Query executor and handler meta. */ static create(bus, queryType, handlerLogic) { const handler = { async handle(query) { return await handlerLogic(query.params, query.metadata); } }; bus.registerQueryHandler(queryType, handler); return { async execute(params, metadata) { return await bus.executeQuery(queryType, params, metadata); }, type: queryType, handler }; } } /** ================================================================================ * Builder for creating and registering event handlers with TypeBus-CQRS. ================================================================================ */ class TypedEventBuilder { /** * Creates and registers an event handler. * @template TEventMap - Event map type * @template T - Event type key * @param {TypeBus<any, any, TEventMap>} bus - The TypeBus-CQRS instance. * @param {T} eventType - The event type. * @param {(data: EventData<TEventMap, T>, aggregateId: string, version: number, metadata?: Record<string, any>) => Promise<void>} handlerLogic - The handler logic. * @returns {object} Event publisher and handler meta. */ static create(bus, eventType, handlerLogic) { const handler = { async handle(event) { return await handlerLogic(event.data, event.aggregateId, event.version, event.metadata); } }; bus.registerEventHandler(eventType, handler); return { async publish(data, aggregateId, version, metadata) { return await bus.publishEvent(eventType, data, aggregateId, version, metadata); }, type: eventType, handler }; } } /** ================================================================================ * Builder for creating a batch of related commands, queries, and events. ================================================================================ */ class BatchBuilder { constructor(bus) { this.bus = bus; this.items = []; } /** * Adds a command to the batch. * @template T - Command type key * @param {string} name - The name of the command. * @param {T} commandType - The command type. * @param {(data: CommandData<TCommandMap, T>, aggregateId: string, metadata?: Record<string, any>) => Promise<CommandResult<TCommandMap, T>>} handlerLogic - The handler logic. * @returns {BatchBuilder<TCommandMap, TQueryMap, TEventMap>} */ addCommand(name, commandType, handlerLogic) { const executor = TypedCommandBuilder.create(this.bus, commandType, handlerLogic); this.items.push({ type: 'command', executor, name }); return this; } /** * Adds a query to the batch. * @template T - Query type key * @param {string} name - The name of the query. * @param {T} queryType - The query type. * @param {(params: QueryParams<TQueryMap, T>, metadata?: Record<string, any>) => Promise<QueryResult<TQueryMap, T>>} handlerLogic - The handler logic. * @returns {BatchBuilder<TCommandMap, TQueryMap, TEventMap>} */ addQuery(name, queryType, handlerLogic) { const executor = TypedQueryBuilder.create(this.bus, queryType, handlerLogic); this.items.push({ type: 'query', executor, name }); return this; } /** * Adds an event handler to the batch. * @template T - Event type key * @param {string} name - The name of the event handler. * @param {T} eventType - The event type. * @param {(data: EventData<TEventMap, T>, aggregateId: string, version: number, metadata?: Record<string, any>) => Promise<void>} handlerLogic - The handler logic. * @returns {BatchBuilder<TCommandMap, TQueryMap, TEventMap>} */ addEventHandler(name, eventType, handlerLogic) { const executor = TypedEventBuilder.create(this.bus, eventType, handlerLogic); this.items.push({ type: 'event', executor, name }); return this; } /** * Builds the batch and returns an object with all executors. * @returns {object} */ build() { const result = {}; this.items.forEach(item => { result[item.name] = item.executor; }); return result; } } /** ================================================================================ * Fluent API builder for TypeBus-CQRS. ================================================================================ */ class FluentBuilder { constructor(bus) { this.bus = bus; } /** * Creates a command with fluent API. * @template T - Command type key * @param {T} commandType - The command type. * @returns {object} Fluent command builder. */ command(commandType) { return { handle: (handlerLogic) => { return TypedCommandBuilder.create(this.bus, commandType, handlerLogic); } }; } /** * Creates a query with fluent API. * @template T - Query type key * @param {T} queryType - The query type. * @returns {object} Fluent query builder. */ query(queryType) { return { handle: (handlerLogic) => { return TypedQueryBuilder.create(this.bus, queryType, handlerLogic); } }; } /** * Creates an event handler with fluent API. * @template T - Event type key * @param {T} eventType - The event type. * @returns {object} Fluent event builder. */ event(eventType) { return { handle: (handlerLogic) => { return TypedEventBuilder.create(this.bus, eventType, handlerLogic); } }; } /** * Creates a batch builder. * @returns {BatchBuilder<TCommandMap, TQueryMap, TEventMap>} */ batch() { return new BatchBuilder(this.bus); } } // ================================================================================ // Factory Functions // ================================================================================ /** * Creates and registers a command handler. * @template TCommandMap - Command map type * @template T - Command type key * @param {TypeBus<TCommandMap, any, any>} bus - The TypeBus-CQRS instance. * @param {T} commandType - The command type. * @param {(data: CommandData<TCommandMap, T>, aggregateId: string, metadata?: Record<string, any>) => Promise<CommandResult<TCommandMap, T>>} handlerLogic - The handler logic. * @returns {object} Command executor. */ function createCommand(bus, commandType, handlerLogic) { return TypedCommandBuilder.create(bus, commandType, handlerLogic); } /** * Creates and registers a query handler. * @template TQueryMap - Query map type * @template T - Query type key * @param {TypeBus<any, TQueryMap, any>} bus - The TypeBus-CQRS instance. * @param {T} queryType - The query type. * @param {(params: QueryParams<TQueryMap, T>, metadata?: Record<string, any>) => Promise<QueryResult<TQueryMap, T>>} handlerLogic - The handler logic. * @returns {object} Query executor. */ function createQuery(bus, queryType, handlerLogic) { return TypedQueryBuilder.create(bus, queryType, handlerLogic); } /** * Creates and registers an event handler. * @template TEventMap - Event map type * @template T - Event type key * @param {TypeBus<any, any, TEventMap>} bus - The TypeBus-CQRS instance. * @param {T} eventType - The event type. * @param {(data: EventData<TEventMap, T>, aggregateId: string, version: number, metadata?: Record<string, any>) => Promise<void>} handlerLogic - The handler logic. * @returns {object} Event publisher. */ function createEventHandler(bus, eventType, handlerLogic) { return TypedEventBuilder.create(bus, eventType, handlerLogic); } /** * Creates a fluent API builder. * @template TCommandMap - Command map type * @template TQueryMap - Query map type * @template TEventMap - Event map type * @param {TypeBus<TCommandMap, TQueryMap, TEventMap>} bus - The TypeBus-CQRS instance. * @returns {FluentBuilder<TCommandMap, TQueryMap, TEventMap>} */ function createFluentBuilder(bus) { return new FluentBuilder(bus); } /** * Middleware for logging message execution in TypeBus-CQRS. * @implements {IMiddleware} */ class LoggingMiddleware { /** * Creates a new LoggingMiddleware instance. * @param {LoggingOptions} [options] */ constructor(options = {}) { this.options = { logLevel: 'info', includeData: false, includeMetadata: false, colorOutput: true, maxDataLength: 200, ...options }; } /** * Executes the middleware logic for logging. * @template T, R * @param {T} message - The message to process. * @param {(message: T) => Promise<R>} next - The next middleware or handler. * @returns {Promise<R>} */ async execute(message, next) { const startTime = process.hrtime.bigint(); const icon = this.getMessageIcon(message.type); if (this.shouldLogStart()) { console.log(this.colorize(`${icon} START: ${message.type}`, 'blue'), { id: message.id, timestamp: message.timestamp.toISOString(), ...this.getExtraLogData(message) }); } try { const result = await next(message); const duration = this.getDuration(startTime); const color = duration > 1000 ? 'yellow' : 'green'; console.log(this.colorize(`${icon} SUCCESS: ${message.type}`, color), { id: message.id, duration: `${duration.toFixed(2)}ms`, ...this.getResultLogData(result) }); return result; } catch (error) { const duration = this.getDuration(startTime); console.error(this.colorize(`${icon} ERROR: ${message.type}`, 'red'), { id: message.id, duration: `${duration.toFixed(2)}ms`, error: error instanceof Error ? error.message : String(error), ...this.getStackTrace(error) }); throw error; } } /** * Determines if the start of message processing should be logged. * @returns {boolean} */ shouldLogStart() { return this.options.logLevel === 'verbose' || this.options.logLevel === 'debug'; } /** * Extracts extra log data from the message (data, params, metadata). * @param {IMessage} message * @returns {object} */ getExtraLogData(message) { const extra = {}; if (this.options.includeData) { const data = message.data || message.params; if (data) { extra.data = this.sanitizeData(data); } } if (this.options.includeMetadata && message.metadata) { extra.metadata = message.metadata; } return extra; } /** * Extracts result log data if logLevel is 'debug'. * @param {any} result * @returns {object} */ getResultLogData(result) { if (this.options.logLevel !== 'debug') return {}; return { result: this.sanitizeResult(result) }; } /** * Extracts stack trace from error if logLevel is 'debug'. * @param {any} error * @returns {object} */ getStackTrace(error) { if (this.options.logLevel !== 'debug') return {}; return { stack: error instanceof Error ? error.stack : undefined }; } /** * Calculates the duration in milliseconds from the given start time. * @param {bigint} startTime * @returns {number} */ getDuration(startTime) { const endTime = process.hrtime.bigint(); return Number(endTime - startTime) / 1000000; } /** * Returns an icon based on the message type. * @param {string} type * @returns {string} */ getMessageIcon(type) { if (this.isCommand(type)) return '📤'; if (this.isQuery(type)) return '📥'; if (this.isEvent(type)) return '📢'; return '💬'; } /** * Determines if the message type is a command. * @param {string} type * @returns {boolean} */ isCommand(type) { return (type.includes('Command') || type.includes('.Create') || type.includes('.Update') || type.includes('.Delete') || type.includes('.Change')); } /** * Determines if the message type is a query. * @param {string} type * @returns {boolean} */ isQuery(type) { return (type.includes('Query') || type.includes('.Get') || type.includes('.Search') || type.includes('.Find')); } /** * Determines if the message type is an event. * @param {string} type * @returns {boolean} */ isEvent(type) { return (type.includes('Event') || type.includes('.Created') || type.includes('.Updated') || type.includes('.Changed')); } /** * Adds color to the log output if enabled. * @param {string} text * @param {string} color * @returns {string} */ colorize(text, color) { if (!this.options.colorOutput) return text; const colors = { red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', reset: '\x1b[0m' }; return `${colors[color] || ''}${text}${colors.reset}`; } /** * Sanitizes data for logging, hiding sensitive fields and limiting length. * @param {any} data * @returns {any} */ sanitizeData(data) { if (!data || typeof data !== 'object') return data; const sanitized = { ...data }; const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth', 'authorization']; for (const field of sensitiveFields) { if (sanitized[field]) { sanitized[field] = '***HIDDEN***'; } } const stringified = JSON.stringify(sanitized); if (stringified.length > this.options.maxDataLength) { return `${stringified.substring(0, this.options.maxDataLength)}...`; } return sanitized; } /** * Sanitizes result for logging, limiting length. * @param {any} result * @returns {any} */ sanitizeResult(result) { if (!result || typeof result !== 'object') return result; const stringified = JSON.stringify(result); if (stringified.length > this.options.maxDataLength) { return `${stringified.substring(0, this.options.maxDataLength)}...`; } return result; } } // middleware/index.ts - Экспорт всех middleware /** * Helper function to quickly set up logging middleware. * @param {Object} [options] * @param {'info'|'debug'|'verbose'} [options.level] * @param {boolean} [options.includeData] * @returns {LoggingMiddleware} */ function withLogging(options) { return new LoggingMiddleware({ logLevel: options?.level || 'info', includeData: options?.includeData || false, includeMetadata: true, colorOutput: true }); } // factory.ts - Фабричные функции для TypeBus-CQRS /** * Factory function for creating a configured TypeBus-CQRS instance. * @template TCommandMap - Command map type * @template TQueryMap - Query map type * @template TEventMap - Event map type * @param {Object} [config] - Optional configuration for the TypeBus-CQRS instance. * @param {boolean} [config.enableLogging] - Enable logging middleware. * @param {'info'|'debug'|'verbose'} [config.logLevel] - Logging level. * @returns {TypeBus<TCommandMap, TQueryMap, TEventMap>} TypeBus-CQRS instance */ function createTypeBus(config) { const bus = new TypeBus({ enableLogging: config?.enableLogging ?? true, logLevel: config?.logLevel ?? 'info' }); if (config?.enableLogging) { bus.use(withLogging({ level: config.logLevel, includeData: config.logLevel === 'debug' })); } return bus; } /** * Utility function for creating a fluent API builder for TypeBus-CQRS. * @template TCommandMap - Command map type * @template TQueryMap - Query map type * @template TEventMap - Event map type * @param {TypeBus<TCommandMap, TQueryMap, TEventMap>} bus - The TypeBus-CQRS instance. * @returns {ReturnType<typeof createFluentBuilder<TCommandMap, TQueryMap, TEventMap>>} */ function fluent(bus) { return createFluentBuilder(bus); } // index.ts - Main export file for TypeBus library // Export all modules /** * Library version string. * @type {string} */ const VERSION = '0.2.0'; export { BatchBuilder, FluentBuilder, LoggingMiddleware, MessageFactory, TypeBus, TypedCommandBuilder, TypedEventBuilder, TypedQueryBuilder, VERSION, createCommand, createEventHandler, createFluentBuilder, createQuery, createTypeBus, fluent, withLogging }; //# sourceMappingURL=index.esm.js.map