feat: update documentation

This commit is contained in:
2025-08-11 14:12:21 +02:00
parent 6da78b043d
commit d3b08b0caa
2 changed files with 138 additions and 53 deletions

172
README.md
View File

@@ -30,35 +30,66 @@ may not be up to date.
```ts ```ts
import { makeReducer } from "@valkyr/event-store"; import { makeReducer } from "@valkyr/event-store";
import type { EventRecord } from "./generated/events.ts"; import type { Events } from "./events.ts";
const reducer = makeReducer< export const userReducer = makeReducer<Events, UserState>(
{
name: string;
email: string;
},
EventRecord
>(
(state, event) => { (state, event) => {
switch (event.type) { switch (event.type) {
case "user:created": { case "user:created": {
state.name = `${event.data.name.given} ${event.data.name.family}`; state.name.given = event.data.name?.given ?? "";
state.name.family = event.data.name?.family ?? "";
state.email = event.data.email; state.email = event.data.email;
break; break;
} }
case "user:name:given-set": {
state.name.given = event.data;
break;
}
case "user:name:family-set": {
state.name.family = event.data;
break;
}
case "user:email-set": { case "user:email-set": {
state.email = event.data.email; state.email = event.data;
break;
}
case "user:activated": {
state.active = true;
break;
}
case "user:deactivated": {
state.active = false;
break; break;
} }
} }
return state; return state;
}, },
"user",
() => ({ () => ({
name: "", name: {
given: "",
family: "",
},
email: "", email: "",
active: true,
posts: {
list: [],
count: 0,
},
}), }),
); );
type UserState = {
name: {
given: string;
family: string;
};
email: string;
active: boolean;
posts: {
list: string[];
count: number;
};
};
``` ```
### Aggreates ### Aggreates
@@ -70,48 +101,89 @@ The benefit of this is that we can create various helper methods on the aggregat
query the aggregated state. query the aggregated state.
```ts ```ts
import { AggregateRoot, makeAggregateReducer } from "@valkyr/event-store"; import { AggregateRoot } from "@valkyr/event-store";
import type { EventRecord } from "./generated/events.ts"; import type { Events } from "./events.ts";
import { eventStore } from "./event-store.ts";
export class User extends AggregateRoot<EventRecord> { export class User extends AggregateRoot<Events> {
name!: Name; static override readonly name = "user";
email!: string;
name: Name = {
given: "",
family: "",
};
email: string = "";
active: boolean = true;
posts: UserPosts = {
list: [],
count: 0,
};
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Factories // Reducer
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
static #reducer = makeAggregateReducer(User, "user"); with(event: Events["$events"][number]["$record"]) {
static async getById(userId: string): Promise<User | undefined> {
return eventStore.reduce({ stream: userId, reducer: this.#reducer });
}
// -------------------------------------------------------------------------
// Folder
// -------------------------------------------------------------------------
with(event: EventRecord) {
switch (event.type) { switch (event.type) {
case "user:created": { case "user:name:given-set": {
this.name = event.data.name; this.name.given = event.data;
this.email = event.data.email; break;
}
case "user:name:family-set": {
this.name.family = event.data;
break; break;
} }
case "user:email-set": { case "user:email-set": {
this.email = event.data.email; this.email = event.data;
break;
}
case "user:activated": {
this.active = true;
break;
}
case "user:deactivated": {
this.active = false;
break; break;
} }
} }
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Utilities // Actions
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
fullName() { setGivenName(given: string): this {
return this.push({
type: "user:name:given-set",
stream: this.id,
data: given,
meta: { auditor: "foo" },
});
}
setFamilyName(family: string): this {
return this.push({
type: "user:name:family-set",
stream: this.id,
data: family,
meta: { auditor: "foo" },
});
}
setEmail(email: string, auditor: string): this {
return this.push({
type: "user:email-set",
stream: this.id,
data: email,
meta: { auditor },
});
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
fullName(): string {
return `${this.name.given} ${this.name.family}`; return `${this.name.given} ${this.name.family}`;
} }
} }
@@ -120,6 +192,11 @@ type Name = {
given: string; given: string;
family: string; family: string;
}; };
type UserPosts = {
list: string[];
count: number;
};
``` ```
### Projectors ### Projectors
@@ -132,10 +209,27 @@ A projector is registered for a specific event type, and can have multiple handl
types of listeners, `once`, `on`, and `all`. types of listeners, `once`, `on`, and `all`.
```ts ```ts
import { projector } from "./event-store.ts"; import { Projector } from "@valkyr/event-store";
projector.on("user:created", async (record) => { import store from "./event-store.ts";
// do something with the event record ... import type { Events } from "./events.ts";
const projector = new Projector<Events>();
if (hooks.onEventsInserted === undefined) {
store.onEventsInserted(async (records, { batch }) => {
if (batch !== undefined) {
await projector.pushMany(batch, records);
} else {
for (const record of records) {
await projector.push(record, { hydrated: false, outdated: false });
}
}
});
}
projector.on("event", async (record) => {
// handle event
}); });
``` ```

View File

@@ -14,24 +14,15 @@ Once we have defined our configs and printed our events we create a new postgres
import { makePostgresEventStore } from "@valkyr/event-store/postgres"; import { makePostgresEventStore } from "@valkyr/event-store/postgres";
import postgres from "postgres"; import postgres from "postgres";
import { type Event, type EventRecord, events, validators } from "./generated/events.ts"; import { events, type Events } from "./events.ts";
export const eventStore = makePostgresEventStore<Event>({ export const eventStore = new EventStore({
connection: () => postgres("postgres://${string}:${string}@${string}:${number}/${string}"), // lazy loaded connection adapter: new PostgresAdapter(connection, { schema: "event_store" }),
schema: "event_store",
events, events,
validators, hooks,
hooks: {
async onError(error) {
// when the event store throws unhandled errors they will end up in
// this location that can be further logged in the systems own logger
// if onError hook is not provided all unhandled errors are logged
// through the `console.error` method.
},
},
}); });
const projector = new Projector<EventRecord>(); const projector = new Projector<Events>();
eventStore.onEventsInserted(async (records, { batch }) => { eventStore.onEventsInserted(async (records, { batch }) => {
// trigger event side effects here such as sending the records through // trigger event side effects here such as sending the records through