@patrick-radius/eventora
v0.4.8
Published
**Eventora** is a lightweight CQRS and Event Sourcing framework for TypeScript, inspired by Axon Framework. It supports decorators for aggregates, command handlers, and event handlers, and includes support for in-memory and Postgres-based event stores.
Readme
Eventora
Eventora is a lightweight CQRS and Event Sourcing framework for TypeScript, inspired by Axon Framework. It supports decorators for aggregates, command handlers, and event handlers, and includes support for in-memory and Postgres-based event stores.
Features
- Lightweight decorator-based API
- Command and event handler registration
- In-memory and Postgres event store implementations
- Simple registry for mapping commands and events to handlers
- Built-in event rehydration
Installation
npm install @patrick-radius/eventoraUsage
1. Define Events and Commands
// events.ts
export class MealPlanned implements Event {
constructor(public readonly date: string, public readonly meal: string) {}
}
// commands.ts
export class PlanMeal implements Command {
constructor(public readonly aggregateId: string, public readonly date: string, public readonly meal: string) {}
}2. Register Events
// registerEvents.ts
import { registerEvent } from '@patrick-radius/eventora';
import { MealPlanned } from './events';
registerEvent('MealPlanned', MealPlanned);3. Create an Aggregate
// meal.aggregate.ts
import { Aggregate, CommandHandler, EventHandler } from '@patrick-radius/eventora';
import { PlanMeal } from './commands';
import { MealPlanned } from './events';
@Aggregate()
export class MealAggregate {
private plannedMeals: Record<string, string> = {};
@CommandHandler(PlanMeal)
planMeal(command: PlanMeal) {
if (this.plannedMeals[command.date]) {
throw new Error('Meal already planned for this date');
}
return new MealPlanned(command.date, command.meal);
}
@EventHandler(MealPlanned)
applyMealPlanned(event: MealPlanned) {
this.plannedMeals[event.date] = event.meal;
}
}4. Dispatch a Command
import { dispatchCommand } from '@patrick-radius/eventora';
import { PlanMeal } from './commands';
import './registerEvents'; // Important: Register event types first
await dispatchCommand(new PlanMeal('abc-123', '2025-08-04', 'Lasagna'));Event Stores
InMemoryEventStore
import { InMemoryEventStore } from '@patrick-radius/eventora';
const store = new InMemoryEventStore();PostgresEventStore
import { PostgresEventStore } from '@patrick-radius/eventora';
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const store = new PostgresEventStore(pool);MultiTenantPostgresEventStore
import { MultiTenantPostgresEventStore } from '@patrick-radius/eventora';
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
class ContextProvider {
getAccountId(): string {
return 'fe75fc7c-f291-4ffd-afa3-85da2ac3185e';
}
getUserId(): string {
return '00a82bbf-c674-47a8-9cf6-ce37cc07c234';
}
}
const eventStore = new MultiTenantPostgresEventStore(pool, new ContextProvider())```3. Project Events to a Read Model
import { Projector, ProjectionHandler } from 'eventora'
import { pool } from './db' // PostgreSQL connection using pg or equivalent
@Projector()
export class RecipeProjection {
@ProjectionHandler('RecipeRenamed')
async onRecipeRenamed(event: { title: string }, { aggregateId }: { aggregateId: string }) {
await pool.query(
`UPDATE recipe_projection SET title = $1 WHERE id = $2`,
[event.title, aggregateId]
)
}
@ProjectionHandler('RecipeCreated')
async onRecipeCreated(event: { title: string }, { aggregateId }: { aggregateId: string }) {
await pool.query(
`INSERT INTO recipe_projection (id, title) VALUES ($1, $2)`,
[aggregateId, event.title]
)
}
}🧠 Unlike aggregates, projectors are not expected to keep state in memory. Instead, they should write to a fast, queryable read model — such as a Postgres table, Redis cache, or search index.
Development
- Run tests:
npm test - Build:
npm run build - Type check:
npm run typecheck
License
MIT
