nestjs-event-sourcing-lib
Version:
A comprehensive Event Sourcing and CQRS library for NestJS applications
485 lines • 21.8 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 PostgresEventStore_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PostgresEventStore = exports.SnapshotEntity = exports.EventEntity = void 0;
const common_1 = require("@nestjs/common");
const event_store_interface_1 = require("../core/event-store.interface");
const event_1 = require("../core/event");
/**
* Entity for storing events in PostgreSQL
*/
class EventEntity {
}
exports.EventEntity = EventEntity;
/**
* Entity for storing snapshots in PostgreSQL
*/
class SnapshotEntity {
}
exports.SnapshotEntity = SnapshotEntity;
/**
* PostgreSQL Event Store implementation
*/
let PostgresEventStore = PostgresEventStore_1 = class PostgresEventStore extends event_store_interface_1.BaseEventStore {
constructor(options, customDataSource) {
super(options);
this.customDataSource = customDataSource;
this.logger = new common_1.Logger(PostgresEventStore_1.name);
}
/**
* Connects to PostgreSQL
*/
async connect() {
try {
if (this.customDataSource) {
this.dataSource = this.customDataSource;
}
else {
// Dynamic import of TypeORM with availability check
let typeorm;
try {
typeorm = await Promise.resolve().then(() => __importStar(require('typeorm')));
}
catch (error) {
throw new event_store_interface_1.EventStoreConnectionError('TypeORM is required for PostgreSQL Event Store. Please install typeorm package.', error instanceof Error ? error : undefined);
}
this.dataSource = new typeorm.DataSource({
type: 'postgres',
url: this.options.connectionString,
entities: [EventEntity, SnapshotEntity],
synchronize: process.env.NODE_ENV !== 'production',
logging: process.env.NODE_ENV === 'development',
extra: {
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
},
});
if (!this.dataSource.isInitialized) {
await this.dataSource.initialize();
}
}
this.eventRepository = this.dataSource.getRepository(EventEntity);
this.snapshotRepository = this.dataSource.getRepository(SnapshotEntity);
this.setConnected(true);
this.logger.log('PostgreSQL Event Store connected successfully');
}
catch (error) {
this.setConnected(false);
throw new event_store_interface_1.EventStoreConnectionError(`Failed to connect to PostgreSQL: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined);
}
}
/**
* Disconnects from PostgreSQL
*/
async disconnect() {
try {
if (this.dataSource && this.dataSource.isInitialized && !this.customDataSource) {
await this.dataSource.destroy();
}
this.setConnected(false);
this.dataSource = undefined;
this.eventRepository = undefined;
this.snapshotRepository = undefined;
this.logger.log('PostgreSQL Event Store disconnected');
}
catch (error) {
this.logger.error('Error disconnecting from PostgreSQL Event Store', error);
}
}
/**
* Saves events for aggregate
*/
async saveEvents(aggregateId, events, expectedVersion) {
if (!this.eventRepository || !this.dataSource) {
throw new event_store_interface_1.EventStoreSaveError('Event Store not connected');
}
if (events.length === 0) {
return {
success: true,
savedEventsCount: 0,
lastEventVersion: expectedVersion || 0,
};
}
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
if (expectedVersion !== undefined) {
const lastEvent = await queryRunner.manager
.getRepository(EventEntity)
.findOne({
where: { aggregate_id: aggregateId },
order: { version: 'DESC' },
});
const actualVersion = lastEvent ? lastEvent.version : 0;
if (actualVersion !== expectedVersion) {
await queryRunner.rollbackTransaction();
return {
success: false,
savedEventsCount: 0,
lastEventVersion: actualVersion,
error: `Version conflict. Expected: ${expectedVersion}, actual: ${actualVersion}`,
};
}
}
const eventEntities = events.map((event, index) => {
const entity = new EventEntity();
entity.event_id = event.id;
entity.aggregate_id = event.aggregateId;
entity.event_type = event.eventType;
entity.payload = event.toJSON();
entity.version = (expectedVersion || 0) + index + 1;
entity.timestamp = event.timestamp;
entity.aggregate_type = event.constructor.name;
return entity;
});
// Save events
await queryRunner.manager.save(EventEntity, eventEntities);
await queryRunner.commitTransaction();
const lastEventVersion = eventEntities[eventEntities.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) {
await queryRunner.rollbackTransaction();
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);
}
finally {
await queryRunner.release();
}
}
/**
* Gets events for aggregate
*/
async getEvents(aggregateId, options) {
if (!this.eventRepository) {
throw new event_store_interface_1.EventStoreLoadError('Event Store not connected');
}
try {
const queryBuilder = this.eventRepository
.createQueryBuilder('event')
.where('event.aggregate_id = :aggregateId', { aggregateId })
.orderBy('event.version', 'ASC');
// Add filters
if ((options === null || options === void 0 ? void 0 : options.fromVersion) !== undefined) {
queryBuilder.andWhere('event.version >= :fromVersion', {
fromVersion: options.fromVersion
});
}
if ((options === null || options === void 0 ? void 0 : options.toVersion) !== undefined) {
queryBuilder.andWhere('event.version <= :toVersion', {
toVersion: options.toVersion
});
}
if ((options === null || options === void 0 ? void 0 : options.eventTypes) && options.eventTypes.length > 0) {
queryBuilder.andWhere('event.event_type IN (:...eventTypes)', {
eventTypes: options.eventTypes
});
}
// Add limit
if ((options === null || options === void 0 ? void 0 : options.limit) !== undefined) {
queryBuilder.limit(options.limit);
}
const eventEntities = await queryBuilder.getMany();
// Convert entities to events
const events = eventEntities.map((entity) => this.entityToEvent(entity));
// Check if there are more events
const hasMore = (options === null || options === void 0 ? void 0 : options.limit) !== undefined && eventEntities.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.snapshotRepository) {
throw new event_store_interface_1.EventStoreSaveError('Event Store not connected');
}
try {
const entity = new SnapshotEntity();
entity.aggregate_id = snapshot.aggregateId;
entity.aggregate_type = snapshot.aggregateType;
entity.version = snapshot.version;
entity.data = snapshot.data;
entity.timestamp = snapshot.timestamp;
await this.snapshotRepository.save(entity);
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.snapshotRepository) {
throw new event_store_interface_1.EventStoreLoadError('Event Store not connected');
}
try {
const entity = await this.snapshotRepository.findOne({
where: { aggregate_id: aggregateId },
});
if (!entity) {
return null;
}
return {
aggregateId: entity.aggregate_id,
aggregateType: entity.aggregate_type,
version: entity.version,
data: entity.data,
timestamp: entity.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.snapshotRepository) {
throw new event_store_interface_1.EventStoreSaveError('Event Store not connected');
}
try {
await this.snapshotRepository.delete({ aggregate_id: 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.eventRepository) {
throw new event_store_interface_1.EventStoreLoadError('Event Store not connected');
}
try {
const queryBuilder = this.eventRepository
.createQueryBuilder('event')
.where('event.event_type = :eventType', { eventType })
.orderBy('event.timestamp', 'ASC');
if ((options === null || options === void 0 ? void 0 : options.limit) !== undefined) {
queryBuilder.limit(options.limit);
}
const eventEntities = await queryBuilder.getMany();
const events = eventEntities.map((entity) => this.entityToEvent(entity));
return {
events,
hasMore: (options === null || options === void 0 ? void 0 : options.limit) !== undefined && eventEntities.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.eventRepository) {
throw new event_store_interface_1.EventStoreLoadError('Event Store not connected');
}
try {
const queryBuilder = this.eventRepository
.createQueryBuilder('event')
.where('event.timestamp >= :from AND event.timestamp <= :to', { from, to })
.orderBy('event.timestamp', 'ASC');
if ((options === null || options === void 0 ? void 0 : options.limit) !== undefined) {
queryBuilder.limit(options.limit);
}
const eventEntities = await queryBuilder.getMany();
const events = eventEntities.map((entity) => this.entityToEvent(entity));
return {
events,
hasMore: (options === null || options === void 0 ? void 0 : options.limit) !== undefined && eventEntities.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.eventRepository || !this.snapshotRepository || !this.dataSource) {
throw new event_store_interface_1.EventStoreSaveError('Event Store not connected');
}
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Delete events
await queryRunner.manager.delete(EventEntity, { aggregate_id: aggregateId });
// Delete snapshot
await queryRunner.manager.delete(SnapshotEntity, { aggregate_id: aggregateId });
await queryRunner.commitTransaction();
this.logger.debug(`Aggregate ${aggregateId} deleted`);
}
catch (error) {
await queryRunner.rollbackTransaction();
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);
}
finally {
await queryRunner.release();
}
}
/**
* Gets Event Store statistics
*/
async getStats() {
if (!this.eventRepository || !this.snapshotRepository) {
throw new event_store_interface_1.EventStoreLoadError('Event Store not connected');
}
try {
const [totalEvents, totalAggregates, totalSnapshots, oldestEvent, newestEvent] = await Promise.all([
this.eventRepository.count(),
this.eventRepository
.createQueryBuilder('event')
.select('COUNT(DISTINCT event.aggregate_id)', 'count')
.getRawOne()
.then((result) => parseInt(result.count)),
this.snapshotRepository.count(),
this.eventRepository.findOne({ order: { timestamp: 'ASC' } }),
this.eventRepository.findOne({ order: { timestamp: 'DESC' } }),
]);
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.eventRepository || !this.snapshotRepository) {
throw new event_store_interface_1.EventStoreSaveError('Event Store not connected');
}
try {
await this.snapshotRepository.clear();
await this.eventRepository.clear();
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.eventRepository) {
return false;
}
// Simple query to check connection
await this.eventRepository.query('SELECT 1');
return true;
}
catch (error) {
this.logger.error('Event Store health check failed', error);
return false;
}
}
/**
* Converts EventEntity to Event
*/
entityToEvent(entity) {
// Create base event from payload
const eventData = entity.payload;
// Create new event instance
const event = Object.create(event_1.Event.prototype);
Object.assign(event, eventData);
return event;
}
};
exports.PostgresEventStore = PostgresEventStore;
exports.PostgresEventStore = PostgresEventStore = PostgresEventStore_1 = __decorate([
(0, common_1.Injectable)(),
__param(1, (0, common_1.Optional)()),
__metadata("design:paramtypes", [Object, Object])
], PostgresEventStore);
//# sourceMappingURL=postgres.event-store.js.map