Skip to content

ArkerLabs/event-sourcing-nestjs

Repository files navigation

✨ Event Sourcing for Nestjs

Library that implements event sourcing using NestJS and his CQRS library.

⭐️ Features

  • StoreEventBus: A class that replaces Nest's EventBus to also persists events in mongodb.
  • StoreEventPublisher: A class that replaces Nest's EventPublisher.
  • ViewUpdaterHandler: The EventBus will also delegate the Events to his View Updaters, so you can update your read database.
  • Replay: You can re-run stored events. This will only trigger the view updater handlers to reconstruct your read db.
  • EventStore: Get history of events for an aggregate.

📖 Contents

🛠 Installation

npm install event-sourcing-nestjs @nestjs/cqrs --save

Usage

Importing

app.module.ts

import { Module } from '@nestjs/common';
import { EventSourcingModule } from 'event-sourcing-nestjs';

@Module({
  imports: [
    EventSourcingModule.forRoot({
      mongoURL: 'mongodb://localhost:27017/eventstore',
    }),
  ],
})
export class ApplicationModule {}

Importing it in your modules

import { Module } from '@nestjs/common';
import { EventSourcingModule } from 'event-sourcing-nestjs';

@Module({
  imports: [
    EventSourcingModule.forFeature(),
  ],
})
export class UserModule {}

Events

Your events must extend the abstract class StorableEvent.

export class UserCreatedEvent extends StorableEvent {
    eventAggregate = 'user';
    eventVersion = 1;
    id = '_id_';
}

Event emitter

Instead of using Nest's EventBus use StoreEventBus, so events will persist before their handlers are executed.

import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { StoreEventBus } from 'event-sourcing-nestjs';

@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {

    constructor(
        private readonly eventBus: StoreEventBus,
    ) {}

    async execute(command: CreateUserCommand) {
        this.eventBus.publish(new UserCreatedEvent(command.name));
    }

}

Event Publisher

Use StoreEventPublisher if you want to dispatch events from your AggregateRoot and store it before calling their handlers.

import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { HeroRepository } from '../../repository/hero.repository';
import { KillDragonCommand } from '../impl/kill-dragon.command';
import { StoreEventPublisher } from 'event-sourcing-nestjs';

@CommandHandler(KillDragonCommand)
export class KillDragonHandler implements ICommandHandler<KillDragonCommand> {
  constructor(
    private readonly repository: HeroRepository,
    private readonly publisher: StoreEventPublisher,
  ) {}

  async execute(command: KillDragonCommand) {
    const { heroId, dragonId } = command;
    const hero = this.publisher.mergeObjectContext(
      await this.repository.findOneById(heroId),
    );
    hero.killEnemy(dragonId);
    hero.commit();
  }
}

Get event history

Reconstruct an aggregate getting his event history.

const aggregate = 'user';
const id = '_id_';
console.log(await this.eventStore.getEvents(aggregate, id));

Full example

hero-killed-dragon.event.ts

import { StorableEvent } from 'event-sourcing-nestjs';

export class HeroKilledDragonEvent extends StorableEvent {

  eventAggregate = 'hero';
  eventVersion = 1;
  
  constructor(
    public readonly id: string,
    public readonly dragonId: string,
  ) {
    super();
  }
}

hero.model.ts

import { AggregateRoot } from '@nestjs/cqrs';

export class Hero extends AggregateRoot {

  public readonly id: string;

  public dragonsKilled: string[] = [];

  constructor(id: string) {
    super();
    this.id = id;
  }

  killEnemy(enemyId: string) {
    this.apply(new HeroKilledDragonEvent(this.id, enemyId));
  }

  onHeroKilledDragonEvent(event: HeroKilledDragonEvent) {
    this.dragonsKilled.push(event.dragonId);
  }

}

hero.repository.ts

import { Injectable } from '@nestjs/common';
import { Hero } from '../models/hero.model';
import { EventStore } from 'event-sourcing-nestjs';

@Injectable()
export class HeroRepository {

  constructor(
    private readonly eventStore: EventStore,
  ) {}

  async findOneById(id: string): Promise<Hero> {
    const hero = new Hero(id);
    hero.loadFromHistory(await this.eventStore.getEvents('hero', id));
    return hero;
  }
}

View updaters

State of the art

State of the art

After emitting an event, use a view updater to update the read database state. This view updaters will be used to recontruct the db if needed.

Read more info about the Materialized View pattern here

import { IViewUpdater, ViewUpdaterHandler } from 'event-sourcing-nestjs';

@ViewUpdaterHandler(UserCreatedEvent)
export class UserCreatedUpdater implements IViewUpdater<UserCreatedEvent> {

    async handle(event: UserCreatedEvent) {
        // Save user into our view db
    }
}

Reconstructing the view db

await ReconstructViewDb.run(await NestFactory.create(AppModule.forRoot()));

Examples

You can find a working example using the Materialized View pattern here.

Also a working example with Nest aggregates working here.

TODOs

  • Use snapshots, so we can reconstruct the aggregates faster.

📝 Stay in touch

About

NestJS module for implementing Event Sourcing

Resources

Stars

Watchers

Forks

Packages

No packages published