semantically
v1.0.9
Published
CQRS/Event Sourcing library for coding semantically
Readme
semantically ⚡️
A minimal CQRS / Event Sourcing helper library for TypeScript that favors clear, semantic domain code.
- Package:
semantically - Purpose: Provide base classes and decorators for defining Aggregate roots, strongly-typed domain Events, Event Handlers, and a simple AggregateRepository pattern.
Quick summary ✅
- Define events that extend
AggregateEventand annotate them with@Event(...). - Define handlers that extend
EventHandler<T>and annotate with@Handles(...). - Implement aggregates by extending
Aggregate<TId>and useapplyEvent(...)to register uncommitted domain events. - Use an
AggregateRepositoryimplementation which takes a list of handlers and calls them on save, then commits events on the aggregate.
Installation
npm install semantically
# dev scripts
npm test
npm run buildUsage examples 🔧
Defining events
import { AggregateEvent, Event } from 'semantically';
@Event(UserCreatedEvent)
class UserCreatedEvent extends AggregateEvent {
constructor(public readonly id: string, public readonly email: string) {
super(1); // event version
}
}Events get:
eventId(uuid),recordedAt(Date),version(number)
The @Event decorator sets an internal __name property used to match handlers to events.
Aggregate roots
import { Aggregate } from 'semantically';
class UserAggregate extends Aggregate<string> {
private email!: string;
constructor(id: string, email: string) {
super(id);
this.registerEventHandlers();
this.applyEvent(new UserCreatedEvent(id, email));
}
override registerEventHandlers(): void {
this.on(UserCreatedEvent, (evt) => {
this.email = evt.email;
});
}
updateEmail(newEmail: string) {
if (this.email !== newEmail) {
this.applyEvent(new UserEmailUpdatedEvent(this.id, newEmail, this.version));
}
}
}Notes:
registerEventHandlers()is where you wire up all event listeners usingthis.on(...). Call it at the top of your constructor.applyEvent(event)adds an uncommitted domain event to the aggregate.- The
aggregateexposesgetEvents()to read uncommitted events. - Call
commit(event)(or let a repository call it) to emit the event and remove it from the uncommitted list.
Snapshot rehydration
When loading an aggregate from a read store rather than replaying events, use the snapshot overload of on. Pass only a listener with a type parameter — the library automatically routes it to the 'snapshot' event name:
type UserSnapshot = {
id: string;
email: string;
version: number;
};
override registerEventHandlers(): void {
this.on(UserCreatedEvent, (evt) => {
this.email = evt.email;
});
this.on<UserSnapshot>((snapshot) => {
this.email = snapshot.email;
this.version = snapshot.version;
});
}The type parameter shapes the listener argument — no extra base class or decorator needed. This overload is visually distinct from regular event handlers, making it clear that snapshot loading is a special case.
Event handlers and decorators
import { Handles, EventHandler } from 'semantically';
@Handles(UserCreatedEvent)
class SendWelcomeEmail extends EventHandler<UserCreatedEvent> {
async handle(event: UserCreatedEvent): Promise<void> {
// send email or persist projection etc.
}
}The @Handles decorator sets an internal __handles property so the repository can route events to matching handlers.
AggregateRepository
Implement a repository by extending AggregateRepository<TId, T> and providing a get(id) method.
class UserRepository extends AggregateRepository<string, UserAggregate> {
async get(id: string): Promise<UserAggregate | null> {
// load persisted state and return rehydrated aggregate or null
}
}
// instantiate with handlers
const repo = new UserRepository([new SendWelcomeEmail()]);
// when saving an aggregate the repo runs handlers and commits events
await repo.save(userAggregate);API Reference
Exports (from index.ts):
Aggregate<TId>- base class for aggregatesAggregateEvent- base class for domain eventsEventHandler<TEvent>- base class for handlersAggregateRepository<TId, T extends Aggregate<TId>>- repository base class@Event(...)decorator - annotate event classes@Handles(...)decorator - annotate handler classes
Tests & Development 🧪
- Run tests:
npm test(usesjest) - Build:
npm run build(emitsdist/)
The repo contains comprehensive unit tests that demonstrate intended usage patterns (see src/*.spec.ts).
Notes & Implementation details ⚠️
- This library relies on TypeScript decorators; ensure
tsconfig.jsonenablesexperimentalDecoratorsandemitDecoratorMetadata. AggregateEventassigns a GUID and recorded timestamp for each event.- The event emitter is fully encapsulated — consumers never interact with it directly. Use
this.on(...)inregisterEventHandlers()to subscribe, andapplyEvent(...)/commit(...)to publish. - The
AggregateRepository.saveimplementation iterates uncommitted events and callshandler.handle(...)when a handler's__handlesmatches the event__name(set by@Event).
Contributing & License
- MIT © Matthew Krizanac
- Contributions welcome; follow existing patterns in tests for examples.
