Library that implements event sourcing using NestJS and his CQRS library.
- 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.
npm install event-sourcing-nestjs @nestjs/cqrs --save
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 {}
Your events must extend the abstract class StorableEvent.
export class UserCreatedEvent extends StorableEvent {
eventAggregate = 'user';
eventVersion = 1;
id = '_id_';
}
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));
}
}
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();
}
}
Reconstruct an aggregate getting his event history.
const aggregate = 'user';
const id = '_id_';
console.log(await this.eventStore.getEvents(aggregate, id));
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;
}
}
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
}
}
await ReconstructViewDb.run(await NestFactory.create(AppModule.forRoot()));
You can find a working example using the Materialized View pattern here.
Also a working example with Nest aggregates working here.
- Use snapshots, so we can reconstruct the aggregates faster.
- Author - Nytyr
- Website - https://arkerlabs.com/