UNPKG

nestjs-event-sourcing-lib

Version:

A comprehensive Event Sourcing and CQRS library for NestJS applications

516 lines (402 loc) 12 kB
# nestjs-event-sourcing-lib A comprehensive Event Sourcing and CQRS library for NestJS applications with TypeScript support. [![npm version](https://badge.fury.io/js/nestjs-event-sourcing-lib.svg)](https://badge.fury.io/js/nestjs-event-sourcing-lib) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) ## Features - 🚀 **Production Ready**: Type-safe Event Sourcing implementation - 🏗️ **Modular Architecture**: Support for multiple Event Store backends - 🔧 **NestJS Integration**: Seamless integration with NestJS DI system - 📊 **CQRS Support**: Command and Query Responsibility Segregation - 🎯 **TypeScript First**: Full TypeScript support with strict typing - 🔄 **Snapshot Support**: Aggregate snapshot functionality for performance - 🛡️ **Error Handling**: Comprehensive error handling with retry policies - 📈 **Scalable**: Designed for enterprise-grade applications ## Supported Event Stores - ✅ **PostgreSQL** (via TypeORM) - ✅ **MongoDB** (via MongoDB driver) - ✅ **Prisma** (via Prisma Client) - 🚧 **EventStoreDB** (planned) ## Installation ```bash npm install nestjs-event-sourcing-lib # For PostgreSQL support npm install typeorm pg # For MongoDB support npm install mongodb # For Prisma support npm install prisma @prisma/client ``` ## Quick Start ### 1. Register the Module ```typescript import { Module } from '@nestjs/common'; import { EventSourcingModule } from 'nestjs-event-sourcing-lib'; @Module({ imports: [ EventSourcingModule.forRoot({ eventStore: { type: 'postgres', connectionString: 'postgresql://user:password@localhost:5432/eventstore', }, }), ], }) export class AppModule {} ``` ### 2. Create Events ```typescript import { Event } from 'nestjs-event-sourcing-lib'; export class UserCreatedEvent extends Event { constructor( public readonly userId: string, public readonly email: string, public readonly name: string, ) { super(); } } export class UserEmailChangedEvent extends Event { constructor( public readonly userId: string, public readonly newEmail: string, ) { super(); } } ``` ### 3. Create Commands ```typescript import { Command } from 'nestjs-event-sourcing-lib'; import { IsEmail, IsNotEmpty } from 'class-validator'; export class CreateUserCommand extends Command { @IsNotEmpty() userId: string; @IsEmail() email: string; @IsNotEmpty() name: string; } export class ChangeUserEmailCommand extends Command { @IsNotEmpty() userId: string; @IsEmail() newEmail: string; } ``` ### 4. Create Aggregate ```typescript import { AggregateRoot } from 'nestjs-event-sourcing-lib'; import { UserCreatedEvent, UserEmailChangedEvent } from './events'; export class UserAggregate extends AggregateRoot { private userId: string; private email: string; private name: string; constructor() { super(); } static create(userId: string, email: string, name: string): UserAggregate { const user = new UserAggregate(); user.apply(new UserCreatedEvent(userId, email, name)); return user; } changeEmail(newEmail: string): void { if (this.email === newEmail) { return; // No change needed } this.apply(new UserEmailChangedEvent(this.userId, newEmail)); } // Event handlers onUserCreatedEvent(event: UserCreatedEvent): void { this.userId = event.userId; this.email = event.email; this.name = event.name; } onUserEmailChangedEvent(event: UserEmailChangedEvent): void { this.email = event.newEmail; } // Getters getId(): string { return this.userId; } getEmail(): string { return this.email; } getName(): string { return this.name; } } ``` ### 5. Create Command Handlers ```typescript import { Injectable } from '@nestjs/common'; import { CommandHandler } from 'nestjs-event-sourcing-lib'; import { CreateUserCommand, ChangeUserEmailCommand } from './commands'; import { UserAggregate } from './user.aggregate'; @Injectable() export class UserCommandHandler { constructor(private readonly commandHandler: CommandHandler) {} async createUser(command: CreateUserCommand): Promise<void> { const user = UserAggregate.create( command.userId, command.email, command.name ); await this.commandHandler.saveAggregate(user); } async changeUserEmail(command: ChangeUserEmailCommand): Promise<void> { const user = await this.commandHandler.loadAggregate( UserAggregate, command.userId ); user.changeEmail(command.newEmail); await this.commandHandler.saveAggregate(user); } } ``` ### 6. Create Event Handlers ```typescript import { Injectable } from '@nestjs/common'; import { OnEvent } from 'nestjs-event-sourcing-lib'; import { UserCreatedEvent, UserEmailChangedEvent } from './events'; @Injectable() export class UserEventHandler { @OnEvent(UserCreatedEvent) handleUserCreated(event: UserCreatedEvent): void { console.log(`User created: ${event.userId}`); // Update read models, send emails, etc. } @OnEvent(UserEmailChangedEvent) handleUserEmailChanged(event: UserEmailChangedEvent): void { console.log(`User email changed: ${event.userId} -> ${event.newEmail}`); // Update read models, send notifications, etc. } } ``` ### 7. Create Projections (Read Models) ```typescript import { Injectable } from '@nestjs/common'; import { Projection, OnEvent } from 'nestjs-event-sourcing-lib'; import { UserCreatedEvent, UserEmailChangedEvent } from './events'; interface UserView { id: string; email: string; name: string; createdAt: Date; updatedAt: Date; } @Injectable() @Projection('user-view') export class UserProjection extends Projection { private users = new Map<string, UserView>(); @OnEvent(UserCreatedEvent) onUserCreated(event: UserCreatedEvent): void { this.users.set(event.userId, { id: event.userId, email: event.email, name: event.name, createdAt: event.timestamp, updatedAt: event.timestamp, }); } @OnEvent(UserEmailChangedEvent) onUserEmailChanged(event: UserEmailChangedEvent): void { const user = this.users.get(event.userId); if (user) { user.email = event.newEmail; user.updatedAt = event.timestamp; } } getUser(userId: string): UserView | undefined { return this.users.get(userId); } getAllUsers(): UserView[] { return Array.from(this.users.values()); } } ``` ## Configuration ### PostgreSQL Configuration ```typescript EventSourcingModule.forRoot({ eventStore: { type: 'postgres', connectionString: 'postgresql://user:password@localhost:5432/eventstore', }, snapshotFrequency: 10, // Create snapshot every 10 events retryPolicy: { maxRetries: 3, retryDelay: 1000, }, }) ``` ### MongoDB Configuration ```typescript EventSourcingModule.forRoot({ eventStore: { type: 'mongodb', connectionString: 'mongodb://localhost:27017/eventstore', }, }) ``` ### Prisma Configuration First, create your Prisma schema: ```prisma // prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" // or "mysql", "sqlite", etc. url = env("DATABASE_URL") } model Event { id String @id @default(uuid()) aggregateId String aggregateType String eventType String eventData Json eventVersion Int timestamp DateTime @default(now()) @@unique([aggregateId, eventVersion]) @@index([aggregateId]) @@index([aggregateType]) @@index([eventType]) } model Snapshot { id String @id @default(uuid()) aggregateId String @unique aggregateType String data Json version Int timestamp DateTime @default(now()) } ``` Then configure the module: ```typescript EventSourcingModule.forRoot({ eventStore: { type: 'prisma', connectionString: process.env.DATABASE_URL, }, }) ``` ### Async Configuration ```typescript EventSourcingModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService) => ({ eventStore: { type: config.get('EVENT_STORE_TYPE'), connectionString: config.get('EVENT_STORE_CONNECTION_STRING'), }, }), }) ``` ## Advanced Features ### Snapshots Snapshots improve performance by storing aggregate state at specific points: ```typescript // Enable automatic snapshots @AggregateRoot({ snapshotFrequency: 5 }) export class OrderAggregate extends AggregateRoot { // Your aggregate implementation } ``` ### Custom Event Store You can implement your own Event Store by extending the `BaseEventStore`: ```typescript import { BaseEventStore } from 'nestjs-event-sourcing-lib'; export class CustomEventStore extends BaseEventStore { async connect(): Promise<void> { // Your connection logic } async saveEvents(/* ... */): Promise<SaveEventsResult> { // Your save logic } // Implement other required methods... } ``` ### Error Handling The library provides comprehensive error handling: ```typescript import { EventStoreConnectionError, EventStoreSaveError, EventStoreLoadError } from 'nestjs-event-sourcing-lib'; try { await this.commandHandler.saveAggregate(aggregate); } catch (error) { if (error instanceof EventStoreSaveError) { // Handle save errors } } ``` ## Testing ### Unit Testing Aggregates ```typescript import { UserAggregate } from './user.aggregate'; import { UserCreatedEvent } from './events'; describe('UserAggregate', () => { it('should create user', () => { const user = UserAggregate.create('123', 'test@example.com', 'John'); const events = user.getUncommittedEvents(); expect(events).toHaveLength(1); expect(events[0]).toBeInstanceOf(UserCreatedEvent); }); }); ``` ### Integration Testing ```typescript import { Test } from '@nestjs/testing'; import { EventSourcingModule } from 'nestjs-event-sourcing-lib'; describe('UserCommandHandler', () => { let module: TestingModule; beforeEach(async () => { module = await Test.createTestingModule({ imports: [ EventSourcingModule.forRoot({ eventStore: { type: 'postgres', connectionString: 'postgresql://localhost:5432/test', }, }), ], providers: [UserCommandHandler], }).compile(); }); // Your tests... }); ``` ## Performance ### Optimization Tips 1. **Use Snapshots**: Enable snapshots for aggregates with many events 2. **Batch Operations**: Use the built-in batching for multiple operations 3. **Proper Indexing**: Ensure your Event Store has proper indexes 4. **Connection Pooling**: Configure connection pooling for your database ### Monitoring ```typescript // Get Event Store statistics const stats = await eventStore.getStats(); console.log(`Total events: ${stats.totalEvents}`); console.log(`Total aggregates: ${stats.totalAggregates}`); ``` ## Migration Guide When upgrading between versions, check the [CHANGELOG.md](CHANGELOG.md) for breaking changes and migration instructions. ## Contributing 1. Fork the repository 2. Create a feature branch 3. Add tests for your changes 4. Ensure all tests pass 5. Submit a pull request ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## Support - 📖 [Documentation](https://github.com/mykolabalielov/nest-event-sourcing/wiki) - 🐛 [Issue Tracker](https://github.com/mykolabalielov/nest-event-sourcing/issues) - 💬 [Discussions](https://github.com/mykolabalielov/nest-event-sourcing/discussions) ## Related Projects - [NestJS](https://nestjs.com/) - The Node.js framework used - [TypeORM](https://typeorm.io/) - ORM for PostgreSQL support - [MongoDB](https://mongodb.github.io/node-mongodb-native/) - MongoDB driver - [Prisma](https://prisma.io/) - Next-generation ORM for Node.js and TypeScript