nestjs-event-sourcing-lib
Version:
A comprehensive Event Sourcing and CQRS library for NestJS applications
516 lines (402 loc) • 12 kB
Markdown
# nestjs-event-sourcing-lib
A comprehensive Event Sourcing and CQRS library for NestJS applications with TypeScript support.
[](https://badge.fury.io/js/nestjs-event-sourcing-lib)
[](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 /client
```
## Quick Start
### 1. Register the Module
```typescript
import { Module } from '@nestjs/common';
import { EventSourcingModule } from 'nestjs-event-sourcing-lib';
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 {
userId: string;
email: string;
name: string;
}
export class ChangeUserEmailCommand extends Command {
userId: string;
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';
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';
export class UserEventHandler {
handleUserCreated(event: UserCreatedEvent): void {
console.log(`User created: ${event.userId}`);
// Update read models, send emails, etc.
}
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;
}
export class UserProjection extends Projection {
private users = new Map<string, UserView>();
onUserCreated(event: UserCreatedEvent): void {
this.users.set(event.userId, {
id: event.userId,
email: event.email,
name: event.name,
createdAt: event.timestamp,
updatedAt: event.timestamp,
});
}
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
aggregateId String
aggregateType String
eventType String
eventData Json
eventVersion Int
timestamp DateTime
@
@
@
@
}
model Snapshot {
id String
aggregateId String
aggregateType String
data Json
version Int
timestamp DateTime
}
```
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
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