nestjs-event-sourcing-lib
Version:
A comprehensive Event Sourcing and CQRS library for NestJS applications
473 lines • 20.7 kB
JavaScript
;
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 PrismaEventStore_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PrismaEventStore = void 0;
const common_1 = require("@nestjs/common");
const event_store_interface_1 = require("../core/event-store.interface");
const event_1 = require("../core/event");
/**
* Prisma Event Store implementation
*/
let PrismaEventStore = PrismaEventStore_1 = class PrismaEventStore extends event_store_interface_1.BaseEventStore {
constructor(options, customPrismaClient) {
super(options);
this.customPrismaClient = customPrismaClient;
this.logger = new common_1.Logger(PrismaEventStore_1.name);
}
/**
* Connects to database via Prisma
*/
async connect() {
try {
if (this.customPrismaClient) {
this.prisma = this.customPrismaClient;
}
else {
// Dynamic import of Prisma Client with availability check
let PrismaClientConstructor;
try {
const prismaModule = await Promise.resolve(`${'@prisma/client'}`).then(s => __importStar(require(s)));
PrismaClientConstructor = prismaModule.PrismaClient;
}
catch (error) {
throw new event_store_interface_1.EventStoreConnectionError('Prisma Client is required for Prisma Event Store. Please install @prisma/client package.', error instanceof Error ? error : undefined);
}
// Create Prisma client with connection string from options
this.prisma = new PrismaClientConstructor({
datasources: {
db: {
url: this.options.connectionString
}
},
log: process.env.NODE_ENV === 'development' ? ['query', 'info', 'warn', 'error'] : ['warn', 'error']
});
// Connect to database
await this.prisma.$connect();
}
this.setConnected(true);
this.logger.log('Prisma Event Store connected successfully');
}
catch (error) {
this.setConnected(false);
throw new event_store_interface_1.EventStoreConnectionError(`Failed to connect with Prisma: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined);
}
}
/**
* Disconnects from database
*/
async disconnect() {
try {
if (this.prisma && !this.customPrismaClient) {
await this.prisma.$disconnect();
}
this.setConnected(false);
this.prisma = undefined;
this.logger.log('Prisma Event Store disconnected');
}
catch (error) {
this.logger.error('Error disconnecting from Prisma Event Store', error);
}
}
/**
* Saves events for aggregate
*/
async saveEvents(aggregateId, events, expectedVersion) {
if (!this.prisma) {
throw new event_store_interface_1.EventStoreSaveError('Event Store not connected');
}
if (events.length === 0) {
return {
success: true,
savedEventsCount: 0,
lastEventVersion: expectedVersion || 0,
};
}
try {
const result = await this.prisma.$transaction(async (prisma) => {
// Check version conflict if expected version is provided
if (expectedVersion !== undefined) {
const lastEvent = await prisma.event.findFirst({
where: { aggregateId },
orderBy: { version: 'desc' }
});
const actualVersion = lastEvent ? lastEvent.version : 0;
if (actualVersion !== expectedVersion) {
return {
success: false,
savedEventsCount: 0,
lastEventVersion: actualVersion,
error: `Version conflict. Expected: ${expectedVersion}, actual: ${actualVersion}`,
};
}
}
// Prepare event data for insertion
const eventData = events.map((event, index) => ({
id: event.id,
aggregateId: event.aggregateId,
eventType: event.eventType,
payload: event.toJSON(),
version: (expectedVersion || 0) + index + 1,
timestamp: event.timestamp,
aggregateType: event.constructor.name,
correlationId: event.correlationId || null,
causationId: event.causationId || null,
userId: event.userId || null,
metadata: event.metadata || {}
}));
// Save all events
await prisma.event.createMany({
data: eventData
});
const lastEventVersion = eventData[eventData.length - 1].version;
return {
success: true,
savedEventsCount: events.length,
lastEventVersion,
};
});
this.logger.debug(`Saved ${events.length} events for aggregate ${aggregateId}`, { aggregateId, eventsCount: events.length, lastVersion: result.lastEventVersion });
return result;
}
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.prisma) {
throw new event_store_interface_1.EventStoreLoadError('Event Store not connected');
}
try {
// Build where clause
const where = { aggregateId };
if ((options === null || options === void 0 ? void 0 : options.fromVersion) !== undefined) {
where.version = { ...where.version, gte: options.fromVersion };
}
if ((options === null || options === void 0 ? void 0 : options.toVersion) !== undefined) {
where.version = { ...where.version, lte: options.toVersion };
}
if ((options === null || options === void 0 ? void 0 : options.eventTypes) && options.eventTypes.length > 0) {
where.eventType = { in: options.eventTypes };
}
// Get events
const eventRecords = await this.prisma.event.findMany({
where,
orderBy: { version: 'asc' },
take: options === null || options === void 0 ? void 0 : options.limit,
});
// Convert to Event objects
const events = eventRecords.map((record) => this.recordToEvent(record));
// Check if there are more events
const hasMore = (options === null || options === void 0 ? void 0 : options.limit) !== undefined && eventRecords.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.prisma) {
throw new event_store_interface_1.EventStoreSaveError('Event Store not connected');
}
try {
await this.prisma.eventSnapshot.upsert({
where: { aggregateId: snapshot.aggregateId },
update: {
aggregateType: snapshot.aggregateType,
version: snapshot.version,
data: snapshot.data,
timestamp: snapshot.timestamp,
},
create: {
aggregateId: snapshot.aggregateId,
aggregateType: snapshot.aggregateType,
version: snapshot.version,
data: snapshot.data,
timestamp: snapshot.timestamp,
},
});
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.prisma) {
throw new event_store_interface_1.EventStoreLoadError('Event Store not connected');
}
try {
const snapshot = await this.prisma.eventSnapshot.findUnique({
where: { aggregateId },
});
if (!snapshot) {
return null;
}
return {
aggregateId: snapshot.aggregateId,
aggregateType: snapshot.aggregateType,
version: snapshot.version,
data: snapshot.data,
timestamp: snapshot.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.prisma) {
throw new event_store_interface_1.EventStoreSaveError('Event Store not connected');
}
try {
await this.prisma.eventSnapshot.delete({
where: { 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.prisma) {
throw new event_store_interface_1.EventStoreLoadError('Event Store not connected');
}
try {
const eventRecords = await this.prisma.event.findMany({
where: { eventType },
orderBy: { timestamp: 'asc' },
take: options === null || options === void 0 ? void 0 : options.limit,
});
const events = eventRecords.map((record) => this.recordToEvent(record));
return {
events,
hasMore: (options === null || options === void 0 ? void 0 : options.limit) !== undefined && eventRecords.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.prisma) {
throw new event_store_interface_1.EventStoreLoadError('Event Store not connected');
}
try {
const eventRecords = await this.prisma.event.findMany({
where: {
timestamp: {
gte: from,
lte: to,
},
},
orderBy: { timestamp: 'asc' },
take: options === null || options === void 0 ? void 0 : options.limit,
});
const events = eventRecords.map((record) => this.recordToEvent(record));
return {
events,
hasMore: (options === null || options === void 0 ? void 0 : options.limit) !== undefined && eventRecords.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.prisma) {
throw new event_store_interface_1.EventStoreSaveError('Event Store not connected');
}
try {
await this.prisma.$transaction(async (prisma) => {
// Delete events
await prisma.event.deleteMany({
where: { aggregateId },
});
// Delete snapshot
await prisma.eventSnapshot.deleteMany({
where: { 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.prisma) {
throw new event_store_interface_1.EventStoreLoadError('Event Store not connected');
}
try {
const [totalEvents, aggregateCount, totalSnapshots, oldestEvent, newestEvent] = await Promise.all([
this.prisma.event.count(),
this.prisma.event.aggregate({
_count: {
aggregateId: true,
},
by: ['aggregateId'],
}).then((results) => results.length),
this.prisma.eventSnapshot.count(),
this.prisma.event.findFirst({
orderBy: { timestamp: 'asc' },
select: { timestamp: true },
}),
this.prisma.event.findFirst({
orderBy: { timestamp: 'desc' },
select: { timestamp: true },
}),
]);
return {
totalEvents,
totalAggregates: aggregateCount,
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.prisma) {
throw new event_store_interface_1.EventStoreSaveError('Event Store not connected');
}
try {
await this.prisma.$transaction(async (prisma) => {
await prisma.eventSnapshot.deleteMany();
await prisma.event.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.prisma) {
return false;
}
// Simple query to check connection
await this.prisma.$queryRaw `SELECT 1`;
return true;
}
catch (error) {
this.logger.error('Event Store health check failed', error);
return false;
}
}
/**
* Converts database record to Event
*/
recordToEvent(record) {
// Create base event from payload
const eventData = record.payload;
// Create new event instance
const event = Object.create(event_1.Event.prototype);
Object.assign(event, eventData);
return event;
}
};
exports.PrismaEventStore = PrismaEventStore;
exports.PrismaEventStore = PrismaEventStore = PrismaEventStore_1 = __decorate([
(0, common_1.Injectable)(),
__param(1, (0, common_1.Optional)()),
__metadata("design:paramtypes", [Object, Object])
], PrismaEventStore);
//# sourceMappingURL=prisma.event-store.js.map