UNPKG

nestjs-event-sourcing-lib

Version:

A comprehensive Event Sourcing and CQRS library for NestJS applications

239 lines 10.2 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var CommandHandler_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.CommandHandler = exports.OptimisticConcurrencyError = exports.CommandValidationError = exports.CommandExecutionError = void 0; const common_1 = require("@nestjs/common"); const event_publisher_1 = require("./event.publisher"); /** * Command execution error */ class CommandExecutionError extends Error { constructor(message, commandId, aggregateId, originalError) { super(`Command execution failed: ${message}`); this.commandId = commandId; this.aggregateId = aggregateId; this.originalError = originalError; this.name = 'CommandExecutionError'; } } exports.CommandExecutionError = CommandExecutionError; /** * Command validation error */ class CommandValidationError extends Error { constructor(message, commandId, validationErrors) { super(`Command validation failed: ${message}`); this.commandId = commandId; this.validationErrors = validationErrors; this.name = 'CommandValidationError'; } } exports.CommandValidationError = CommandValidationError; /** * Optimistic concurrency error */ class OptimisticConcurrencyError extends Error { constructor(aggregateId, expectedVersion, actualVersion) { super(`Optimistic concurrency conflict for aggregate ${aggregateId}. ` + `Expected version: ${expectedVersion}, actual version: ${actualVersion}`); this.name = 'OptimisticConcurrencyError'; } } exports.OptimisticConcurrencyError = OptimisticConcurrencyError; /** * Service for handling commands */ let CommandHandler = CommandHandler_1 = class CommandHandler { constructor(eventStore, eventBus) { this.eventStore = eventStore; this.eventBus = eventBus; this.logger = new common_1.Logger(CommandHandler_1.name); } /** * Executes command for aggregate */ async execute(aggregate, command, context) { var _a; const startTime = Date.now(); try { // Log start of command execution this.logger.debug(`Executing command ${command.getCommandName()} for aggregate ${command.aggregateId}`, { commandId: command.commandId, aggregateId: command.aggregateId }); // Command validation await this.validateCommand(command); // Create context if not provided const commandContext = context || this.createDefaultContext(command); // Load aggregate if needed if (!aggregate.getIsLoaded()) { await this.loadAggregate(aggregate, command.aggregateId); } // Execute command (delegate to aggregate) const handlerMethodName = this.getHandlerMethodName(command); if (typeof aggregate[handlerMethodName] !== 'function') { throw new CommandExecutionError(`Handler method ${handlerMethodName} not found in aggregate`, command.commandId, command.aggregateId); } // Call handler in aggregate await aggregate[handlerMethodName](command, commandContext); // Save events const result = await this.saveAggregate(aggregate); // Publish events if (this.eventBus && result.events && result.events.length > 0) { await this.eventBus.publishAll(result.events); } const executionTime = Date.now() - startTime; this.logger.debug(`Command ${command.getCommandName()} executed successfully in ${executionTime}ms`, { commandId: command.commandId, aggregateId: command.aggregateId, eventsCount: ((_a = result.events) === null || _a === void 0 ? void 0 : _a.length) || 0, executionTime }); return { success: true, result: aggregate, events: result.events, aggregateId: command.aggregateId, version: aggregate.getVersion(), }; } catch (error) { const executionTime = Date.now() - startTime; this.logger.error(`Command ${command.getCommandName()} failed after ${executionTime}ms`, { commandId: command.commandId, aggregateId: command.aggregateId, error: error instanceof Error ? error.message : String(error), executionTime, }); return { success: false, error: error instanceof Error ? error.message : String(error), aggregateId: command.aggregateId, }; } } /** * Validates command */ async validateCommand(command) { try { await command.validate(); } catch (validationErrors) { throw new CommandValidationError('Command validation failed', command.commandId, Array.isArray(validationErrors) ? validationErrors.map(e => e.toString()) : [String(validationErrors)]); } } /** * Creates default context */ createDefaultContext(command) { return { commandId: command.commandId, aggregateId: command.aggregateId, metadata: command.metadata, timestamp: new Date(), }; } /** * Gets handler method name for command */ getHandlerMethodName(command) { const commandName = command.getCommandName(); // Convert from PascalCase to camelCase and add prefix const methodName = commandName.charAt(0).toLowerCase() + commandName.slice(1); return `handle${commandName}`; } /** * Loads aggregate from Event Store */ async loadAggregate(aggregate, aggregateId) { try { // Try to load snapshot const snapshot = await this.eventStore.getSnapshot(aggregateId); if (snapshot) { aggregate.loadFromSnapshot(snapshot); // Load events after snapshot const eventsResult = await this.eventStore.getEvents(aggregateId, { fromVersion: snapshot.version + 1, }); if (eventsResult.events.length > 0) { aggregate.loadFromHistory(eventsResult.events); } } else { // Load all events const events = await this.eventStore.getAllEvents(aggregateId); if (events.length > 0) { aggregate.loadFromHistory(events); } } } catch (error) { throw new CommandExecutionError(`Failed to load aggregate ${aggregateId}`, '', aggregateId, error instanceof Error ? error : new Error(String(error))); } } /** * Saves aggregate in Event Store */ async saveAggregate(aggregate) { const uncommittedEvents = aggregate.getUncommittedEvents(); if (uncommittedEvents.length === 0) { return { events: [], version: aggregate.getVersion() }; } try { const expectedVersion = aggregate.getVersion() - uncommittedEvents.length; const result = await this.eventStore.saveEvents(aggregate.getId(), uncommittedEvents, expectedVersion); if (!result.success) { throw new Error(result.error || 'Failed to save events'); } // Clear uncommitted events aggregate.markEventsAsCommitted(); // Create snapshot if needed await this.createSnapshotIfNeeded(aggregate); return { events: uncommittedEvents, version: aggregate.getVersion(), }; } catch (error) { throw new CommandExecutionError(`Failed to save aggregate ${aggregate.getId()}`, '', aggregate.getId(), error instanceof Error ? error : new Error(String(error))); } } /** * Creates snapshot if needed */ async createSnapshotIfNeeded(aggregate) { // Logic can be added to determine when to create snapshot // For example, every 10 events or under certain conditions const snapshotFrequency = 10; // Can be made configurable if (aggregate.getVersion() % snapshotFrequency === 0) { try { const snapshot = aggregate.createSnapshot(); await this.eventStore.saveSnapshot({ aggregateId: aggregate.getId(), aggregateType: aggregate.constructor.name, version: aggregate.getVersion(), data: snapshot, timestamp: new Date(), }); this.logger.debug(`Snapshot created for aggregate ${aggregate.getId()} at version ${aggregate.getVersion()}`); } catch (error) { // Snapshot creation error should not interrupt command execution this.logger.warn(`Failed to create snapshot for aggregate ${aggregate.getId()}`, error); } } } }; exports.CommandHandler = CommandHandler; exports.CommandHandler = CommandHandler = CommandHandler_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [Object, event_publisher_1.EventBus]) ], CommandHandler); //# sourceMappingURL=command.handler.js.map