UNPKG

nestjs-event-sourcing-lib

Version:

A comprehensive Event Sourcing and CQRS library for NestJS applications

479 lines 21.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); 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 __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var MongoDBEventStore_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.MongoDBEventStore = void 0; const common_1 = require("@nestjs/common"); const event_store_interface_1 = require("../core/event-store.interface"); const event_1 = require("../core/event"); /** * MongoDB Event Store implementation */ let MongoDBEventStore = MongoDBEventStore_1 = class MongoDBEventStore extends event_store_interface_1.BaseEventStore { constructor(options, customClient) { super(options); this.customClient = customClient; this.logger = new common_1.Logger(MongoDBEventStore_1.name); } /** * Connects to MongoDB */ async connect() { try { if (this.customClient) { this.client = this.customClient; } else { // Dynamic import of MongoDB with availability check let mongodb; try { mongodb = await Promise.resolve().then(() => __importStar(require('mongodb'))); } catch (error) { throw new event_store_interface_1.EventStoreConnectionError('MongoDB driver is required for MongoDB Event Store. Please install mongodb package.', error instanceof Error ? error : undefined); } this.client = new mongodb.MongoClient(this.options.connectionString, { maxPoolSize: 20, serverSelectionTimeoutMS: 5000, socketTimeoutMS: 45000, family: 4, }); await this.client.connect(); } // Get database (name from connection string) const dbName = 'eventstore'; // Can be added to options later this.database = this.client.db(dbName); // Get collections this.eventsCollection = this.database.collection('events'); this.snapshotsCollection = this.database.collection('snapshots'); // Create indexes for optimization await this.createIndexes(); this.setConnected(true); this.logger.log('MongoDB Event Store connected successfully'); } catch (error) { this.setConnected(false); throw new event_store_interface_1.EventStoreConnectionError(`Failed to connect to MongoDB: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined); } } /** * Disconnects from MongoDB */ async disconnect() { try { if (this.client && !this.customClient) { await this.client.close(); } this.setConnected(false); this.client = undefined; this.database = undefined; this.eventsCollection = undefined; this.snapshotsCollection = undefined; this.logger.log('MongoDB Event Store disconnected'); } catch (error) { this.logger.error('Error disconnecting from MongoDB Event Store', error); } } /** * Saves events for aggregate */ async saveEvents(aggregateId, events, expectedVersion) { if (!this.eventsCollection) { throw new event_store_interface_1.EventStoreSaveError('Event Store not connected'); } if (events.length === 0) { return { success: true, savedEventsCount: 0, lastEventVersion: expectedVersion || 0, }; } try { // Optimistic concurrency control if (expectedVersion !== undefined) { const lastEvent = await this.eventsCollection.findOne({ aggregateId }, { sort: { version: -1 } }); const actualVersion = lastEvent ? lastEvent.version : 0; if (actualVersion !== expectedVersion) { return { success: false, savedEventsCount: 0, lastEventVersion: actualVersion, error: `Version conflict. Expected: ${expectedVersion}, actual: ${actualVersion}`, }; } } const eventDocs = events.map((event, index) => ({ eventId: event.id, aggregateId: event.aggregateId, eventType: event.eventType, payload: event.toJSON(), version: (expectedVersion || 0) + index + 1, timestamp: event.timestamp, aggregateType: event.constructor.name, })); // Save all events atomically await this.eventsCollection.insertMany(eventDocs); const lastEventVersion = eventDocs[eventDocs.length - 1].version; this.logger.debug(`Saved ${events.length} events for aggregate ${aggregateId}`, { aggregateId, eventsCount: events.length, lastVersion: lastEventVersion }); return { success: true, savedEventsCount: events.length, lastEventVersion, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Failed to save events for aggregate ${aggregateId}`, { aggregateId, eventsCount: events.length, error: errorMessage }); throw new event_store_interface_1.EventStoreSaveError(`Failed to save events for aggregate ${aggregateId}: ${errorMessage}`, error instanceof Error ? error : undefined); } } /** * Gets events for aggregate */ async getEvents(aggregateId, options) { if (!this.eventsCollection) { throw new event_store_interface_1.EventStoreLoadError('Event Store not connected'); } try { // Create filter const filter = { aggregateId }; if ((options === null || options === void 0 ? void 0 : options.fromVersion) !== undefined) { filter.version = { $gte: options.fromVersion }; } if ((options === null || options === void 0 ? void 0 : options.toVersion) !== undefined) { filter.version = { ...filter.version, $lte: options.toVersion }; } if ((options === null || options === void 0 ? void 0 : options.eventTypes) && options.eventTypes.length > 0) { filter.eventType = { $in: options.eventTypes }; } // Create query let cursor = this.eventsCollection .find(filter) .sort({ version: 1 }); if ((options === null || options === void 0 ? void 0 : options.limit) !== undefined) { cursor = cursor.limit(options.limit); } const eventDocs = await cursor.toArray(); // Convert documents to events const events = eventDocs.map((doc) => this.documentToEvent(doc)); // Check if there are more events const hasMore = (options === null || options === void 0 ? void 0 : options.limit) !== undefined && eventDocs.length === options.limit; this.logger.debug(`Retrieved ${events.length} events for aggregate ${aggregateId}`, { aggregateId, eventsCount: events.length, hasMore }); return { events, hasMore, totalCount: events.length, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Failed to get events for aggregate ${aggregateId}`, { aggregateId, error: errorMessage }); throw new event_store_interface_1.EventStoreLoadError(`Failed to get events for aggregate ${aggregateId}: ${errorMessage}`, error instanceof Error ? error : undefined); } } /** * Saves aggregate snapshot */ async saveSnapshot(snapshot) { if (!this.snapshotsCollection) { throw new event_store_interface_1.EventStoreSaveError('Event Store not connected'); } try { const doc = { aggregateId: snapshot.aggregateId, aggregateType: snapshot.aggregateType, version: snapshot.version, data: snapshot.data, timestamp: snapshot.timestamp, }; // Use upsert to replace existing snapshot await this.snapshotsCollection.updateOne({ aggregateId: snapshot.aggregateId }, { $set: doc }, { upsert: true }); this.logger.debug(`Snapshot saved for aggregate ${snapshot.aggregateId} at version ${snapshot.version}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new event_store_interface_1.EventStoreSaveError(`Failed to save snapshot for aggregate ${snapshot.aggregateId}: ${errorMessage}`, error instanceof Error ? error : undefined); } } /** * Gets the latest snapshot for aggregate */ async getSnapshot(aggregateId) { if (!this.snapshotsCollection) { throw new event_store_interface_1.EventStoreLoadError('Event Store not connected'); } try { const doc = await this.snapshotsCollection.findOne({ aggregateId }); if (!doc) { return null; } return { aggregateId: doc.aggregateId, aggregateType: doc.aggregateType, version: doc.version, data: doc.data, timestamp: doc.timestamp, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new event_store_interface_1.EventStoreLoadError(`Failed to get snapshot for aggregate ${aggregateId}: ${errorMessage}`, error instanceof Error ? error : undefined); } } /** * Deletes aggregate snapshot */ async deleteSnapshot(aggregateId) { if (!this.snapshotsCollection) { throw new event_store_interface_1.EventStoreSaveError('Event Store not connected'); } try { await this.snapshotsCollection.deleteOne({ aggregateId }); this.logger.debug(`Snapshot deleted for aggregate ${aggregateId}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new event_store_interface_1.EventStoreSaveError(`Failed to delete snapshot for aggregate ${aggregateId}: ${errorMessage}`, error instanceof Error ? error : undefined); } } /** * Gets events by type */ async getEventsByType(eventType, options) { if (!this.eventsCollection) { throw new event_store_interface_1.EventStoreLoadError('Event Store not connected'); } try { let cursor = this.eventsCollection .find({ eventType }) .sort({ timestamp: 1 }); if ((options === null || options === void 0 ? void 0 : options.limit) !== undefined) { cursor = cursor.limit(options.limit); } const eventDocs = await cursor.toArray(); const events = eventDocs.map((doc) => this.documentToEvent(doc)); return { events, hasMore: (options === null || options === void 0 ? void 0 : options.limit) !== undefined && eventDocs.length === options.limit, totalCount: events.length, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new event_store_interface_1.EventStoreLoadError(`Failed to get events by type ${eventType}: ${errorMessage}`, error instanceof Error ? error : undefined); } } /** * Gets events in time range */ async getEventsByTimeRange(from, to, options) { if (!this.eventsCollection) { throw new event_store_interface_1.EventStoreLoadError('Event Store not connected'); } try { let cursor = this.eventsCollection .find({ timestamp: { $gte: from, $lte: to, }, }) .sort({ timestamp: 1 }); if ((options === null || options === void 0 ? void 0 : options.limit) !== undefined) { cursor = cursor.limit(options.limit); } const eventDocs = await cursor.toArray(); const events = eventDocs.map((doc) => this.documentToEvent(doc)); return { events, hasMore: (options === null || options === void 0 ? void 0 : options.limit) !== undefined && eventDocs.length === options.limit, totalCount: events.length, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new event_store_interface_1.EventStoreLoadError(`Failed to get events by time range: ${errorMessage}`, error instanceof Error ? error : undefined); } } /** * Deletes all events for aggregate */ async deleteAggregate(aggregateId) { if (!this.eventsCollection || !this.snapshotsCollection) { throw new event_store_interface_1.EventStoreSaveError('Event Store not connected'); } try { // Delete events and snapshots await Promise.all([ this.eventsCollection.deleteMany({ aggregateId }), this.snapshotsCollection.deleteOne({ aggregateId }), ]); this.logger.debug(`Aggregate ${aggregateId} deleted`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new event_store_interface_1.EventStoreSaveError(`Failed to delete aggregate ${aggregateId}: ${errorMessage}`, error instanceof Error ? error : undefined); } } /** * Gets Event Store statistics */ async getStats() { if (!this.eventsCollection || !this.snapshotsCollection) { throw new event_store_interface_1.EventStoreLoadError('Event Store not connected'); } try { const [totalEvents, totalSnapshots, oldestEvent, newestEvent, aggregatesResult,] = await Promise.all([ this.eventsCollection.countDocuments(), this.snapshotsCollection.countDocuments(), this.eventsCollection.findOne({}, { sort: { timestamp: 1 } }), this.eventsCollection.findOne({}, { sort: { timestamp: -1 } }), this.eventsCollection.aggregate([ { $group: { _id: '$aggregateId', }, }, { $count: 'totalAggregates', }, ]).toArray(), ]); const totalAggregates = aggregatesResult.length > 0 ? aggregatesResult[0].totalAggregates : 0; return { totalEvents, totalAggregates, totalSnapshots, oldestEvent: oldestEvent === null || oldestEvent === void 0 ? void 0 : oldestEvent.timestamp, newestEvent: newestEvent === null || newestEvent === void 0 ? void 0 : newestEvent.timestamp, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new event_store_interface_1.EventStoreLoadError(`Failed to get Event Store stats: ${errorMessage}`, error instanceof Error ? error : undefined); } } /** * Clears all data (used for testing) */ async clear() { if (!this.eventsCollection || !this.snapshotsCollection) { throw new event_store_interface_1.EventStoreSaveError('Event Store not connected'); } try { await Promise.all([ this.eventsCollection.deleteMany({}), this.snapshotsCollection.deleteMany({}), ]); this.logger.debug('Event Store cleared'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new event_store_interface_1.EventStoreSaveError(`Failed to clear Event Store: ${errorMessage}`, error instanceof Error ? error : undefined); } } /** * Checks Event Store health */ async healthCheck() { try { if (!this.database) { return false; } // Simple query to check connection await this.database.collection('events').findOne({}, { limit: 1 }); return true; } catch (error) { this.logger.error('Event Store health check failed', error); return false; } } /** * Creates indexes for query optimization */ async createIndexes() { if (!this.eventsCollection || !this.snapshotsCollection) { return; } try { // Indexes for events collection await Promise.all([ this.eventsCollection.createIndex({ aggregateId: 1, version: 1 }, { unique: true }), this.eventsCollection.createIndex({ aggregateId: 1 }), this.eventsCollection.createIndex({ eventType: 1 }), this.eventsCollection.createIndex({ timestamp: 1 }), this.eventsCollection.createIndex({ aggregateType: 1 }), ]); // Indexes for snapshots collection await this.snapshotsCollection.createIndex({ aggregateId: 1 }, { unique: true }); this.logger.debug('MongoDB indexes created successfully'); } catch (error) { this.logger.warn('Failed to create some indexes', error); } } /** * Converts EventDocument to Event */ documentToEvent(doc) { // Create base event from payload const eventData = doc.payload; // Create new event instance const event = Object.create(event_1.Event.prototype); Object.assign(event, eventData); return event; } }; exports.MongoDBEventStore = MongoDBEventStore; exports.MongoDBEventStore = MongoDBEventStore = MongoDBEventStore_1 = __decorate([ (0, common_1.Injectable)(), __param(1, (0, common_1.Optional)()), __metadata("design:paramtypes", [Object, Object]) ], MongoDBEventStore); //# sourceMappingURL=mongodb.event-store.js.map