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