Module configuration
Everything in this library is wired together by the EventSourcingModule. It exposes three static methods:
| Method | Where to import it | What it’s for |
|---|---|---|
forRoot(options) | Your root module, once | Configure the module with static options. |
forRootAsync(options) | Your root module, once | Configure the module when the options depend on other providers, e.g. a config service. |
forFeature({ events }) | Any feature module | Register the events that belong to that feature module. |
Root configuration with forRoot
EventSourcingModule.forRoot() accepts the following options. All of them are optional:
| Option | Description |
|---|---|
events | The event classes to register globally. Optional, because feature modules can register their own events with forFeature. |
eventStore | The configuration of the event store. Needs a driver (the store class), can contain useDefaultPool and any option that the driver needs to connect. Defaults to the in-memory event store. |
snapshotStore | The configuration of the snapshot store, which has the same shape as eventStore. Defaults to the in-memory snapshot store. |
The in-memory stores are handy for testing and prototyping, but they lose everything when the process stops. Each integration library exports its own drivers and configuration interfaces, see installation.
import { Module } from '@nestjs/common';
import { EventSourcingModule } from '@ocoda/event-sourcing';
import {
PostgresEventStore,
PostgresSnapshotStore,
type PostgresEventStoreConfig,
type PostgresSnapshotStoreConfig,
} from '@ocoda/event-sourcing-postgres';
@Module({
imports: [
EventSourcingModule.forRoot<PostgresEventStoreConfig, PostgresSnapshotStoreConfig>({
eventStore: {
driver: PostgresEventStore,
connectionString: process.env.DATABASE_URL,
},
snapshotStore: {
driver: PostgresSnapshotStore,
connectionString: process.env.DATABASE_URL,
},
}),
],
})
export class AppModule {}When you don’t want to list any events in the root module, you can leave out events completely and register them in your feature modules. For example, with the default in-memory stores that’s just:
import { Module } from '@nestjs/common';
import { EventSourcingModule } from '@ocoda/event-sourcing';
@Module({
imports: [EventSourcingModule.forRoot({})],
})
export class AppModule {}By default the module creates the default pool (a collection named events and one named snapshots) when it starts. Set useDefaultPool: false on the store configuration when you only want to work with pools per tenant, see multitenancy.
Asynchronous configuration with forRootAsync
Use EventSourcingModule.forRootAsync() when the options depend on something that isn’t available yet when the module is declared, such as environment configuration or a secrets provider. The options are resolved before the stores are created, and can be provided in four ways.
A factory function that returns the options (or a promise of them). Add the modules that provide its dependencies to imports and list those dependencies in inject, in the order of the factory’s parameters.
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { EventSourcingModule } from '@ocoda/event-sourcing';
import {
PostgresEventStore,
PostgresSnapshotStore,
type PostgresEventStoreConfig,
type PostgresSnapshotStoreConfig,
} from '@ocoda/event-sourcing-postgres';
@Module({
imports: [
EventSourcingModule.forRootAsync<PostgresEventStoreConfig, PostgresSnapshotStoreConfig>({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
eventStore: {
driver: PostgresEventStore,
connectionString: config.getOrThrow<string>('DATABASE_URL'),
},
snapshotStore: {
driver: PostgresSnapshotStore,
connectionString: config.getOrThrow<string>('DATABASE_URL'),
},
}),
}),
],
})
export class AppModule {}You can also pass the options object directly with useValue, which comes down to the same as calling forRoot. Since 3.0.1 forRootAsync throws a descriptive error when none of useFactory, useClass, useExisting or useValue is provided.
Registering events per feature module with forFeature
EventSourcingModule.forFeature() and the optional events in forRoot are available since 3.0.0. In earlier versions every event has to be listed in the events of forRoot/forRootAsync.
Every event that ends up in the event store has to be registered, otherwise the library can’t serialize or deserialize it and an UnregisteredEventException is thrown. Instead of collecting all events of your application in the root module, each feature module can register its own events. This keeps a bounded context self-contained: the module that owns the events, aggregates and handlers is also the one that registers them.
In the example below an application has two feature modules, each registering its own events:
import { Module } from '@nestjs/common';
import { EventSourcingModule } from '@ocoda/event-sourcing';
@Module({
imports: [
EventSourcingModule.forFeature({
events: [AccountOpenedEvent, AccountCreditedEvent, AccountDebitedEvent],
}),
],
providers: [AccountRepository, AccountSnapshotRepository, OpenAccountCommandHandler, CreditAccountCommandHandler],
})
export class AccountsModule {}import { Module } from '@nestjs/common';
import { EventSourcingModule } from '@ocoda/event-sourcing';
@Module({
imports: [
EventSourcingModule.forFeature({
events: [TransferRequestedEvent, TransferCompletedEvent],
}),
],
providers: [TransferRepository, RequestTransferCommandHandler, TransferCompletedEventSubscriber],
})
export class TransfersModule {}The root module configures the stores once and imports the feature modules. It doesn’t need to know about their events:
import { Module } from '@nestjs/common';
import { EventSourcingModule } from '@ocoda/event-sourcing';
import { PostgresEventStore, PostgresSnapshotStore } from '@ocoda/event-sourcing-postgres';
@Module({
imports: [
EventSourcingModule.forRoot({
eventStore: { driver: PostgresEventStore, connectionString: process.env.DATABASE_URL },
snapshotStore: { driver: PostgresSnapshotStore, connectionString: process.env.DATABASE_URL },
}),
AccountsModule,
TransfersModule,
],
})
export class AppModule {}A few things to keep in mind:
forFeaturedoesn’t create or export any providers, it only registers events. The stores and buses come from the root module, which is global, see below.- The events are collected in a registry that is shared by the whole Node.js process, and they are added when
forFeatureis called, which happens when the file of the module is loaded. Together with the events of the root module they form the set of registered events at startup. If you create several Nest applications in one process, for example in tests, they share the events registered byforFeature. - Register an event in one place only, either in
forRoot/forRootAsyncor in a singleforFeature.
The module is global
forRoot and forRootAsync register a global module. It provides and exports the EventStore, the SnapshotStore, the EventBus, the CommandBus, the QueryBus and the EventMap. That means you can inject them in any provider of your application without importing EventSourcingModule again.
import { Injectable } from '@nestjs/common';
import { CommandBus, EventStore } from '@ocoda/event-sourcing';
@Injectable()
export class TransferService {
constructor(
private readonly commandBus: CommandBus,
private readonly eventStore: EventStore,
) {}
}Import EventSourcingModule.forRoot() or EventSourcingModule.forRootAsync() once, in your root module. Importing them in several modules can result in separate configurations, each with their own stores and buses.
Discovery at application bootstrap
Command handlers, query handlers, event subscribers, event publishers and event serializers don’t have to be registered anywhere. When the application has bootstrapped, the library scans the providers of all modules of the application, regardless of where forRoot or forFeature were imported, and registers every provider that is decorated with one of these decorators:
| Decorator | Registered in |
|---|---|
@CommandHandler() | the CommandBus |
@QueryHandler() | the QueryBus |
@EventSubscriber() | the EventBus, see event pub/sub |
@EventPublisher() | the EventBus, see event pub/sub |
@EventSerializer() | the EventMap, see event serialization |
Adding them to the providers of any module is enough. Register them as regular (singleton-scoped) providers. Other classes, such as aggregate repositories and snapshot repositories (@Snapshot()), aren’t discovered: they are ordinary providers that you inject where you need them.
The module goes through these steps when the application starts:
- Module initialization: the event store and the snapshot store connect, and the default pools are created (unless
useDefaultPoolisfalse). - Application bootstrap: the providers of all modules are scanned, the handlers, subscribers, publishers and serializers are registered, and the event store is connected to the event bus so appended events get published.
Because the registration happens when the application has bootstrapped, don’t execute commands or queries, or append or read events, from lifecycle hooks such as onModuleInit. Run that logic once the application has started, for example from a request handler or a scheduled job.