nestjs-event-sourcing-lib
Version:
A comprehensive Event Sourcing and CQRS library for NestJS applications
239 lines • 10.2 kB
JavaScript
;
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