# 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
