From d3b08b0caa1485421dbc8defe6f980ae5d876387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoffer=20R=C3=B8dvik?= Date: Mon, 11 Aug 2025 14:12:21 +0200 Subject: [PATCH] feat: update documentation --- README.md | 172 ++++++++++++++++++++++++++++-------- adapters/postgres/README.md | 19 ++-- 2 files changed, 138 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index b17182f..5271722 100644 --- a/README.md +++ b/README.md @@ -30,35 +30,66 @@ may not be up to date. ```ts import { makeReducer } from "@valkyr/event-store"; -import type { EventRecord } from "./generated/events.ts"; +import type { Events } from "./events.ts"; -const reducer = makeReducer< - { - name: string; - email: string; - }, - EventRecord ->( +export const userReducer = makeReducer( (state, event) => { switch (event.type) { 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; 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": { - state.email = event.data.email; + state.email = event.data; + break; + } + case "user:activated": { + state.active = true; + break; + } + case "user:deactivated": { + state.active = false; break; } } return state; }, - "user", () => ({ - name: "", + name: { + given: "", + family: "", + }, email: "", + active: true, + posts: { + list: [], + count: 0, + }, }), ); + +type UserState = { + name: { + given: string; + family: string; + }; + email: string; + active: boolean; + posts: { + list: string[]; + count: number; + }; +}; ``` ### Aggreates @@ -70,48 +101,89 @@ The benefit of this is that we can create various helper methods on the aggregat query the aggregated state. ```ts -import { AggregateRoot, makeAggregateReducer } from "@valkyr/event-store"; +import { AggregateRoot } from "@valkyr/event-store"; -import type { EventRecord } from "./generated/events.ts"; -import { eventStore } from "./event-store.ts"; +import type { Events } from "./events.ts"; -export class User extends AggregateRoot { - name!: Name; - email!: string; +export class User extends AggregateRoot { + static override readonly name = "user"; + + name: Name = { + given: "", + family: "", + }; + email: string = ""; + active: boolean = true; + posts: UserPosts = { + list: [], + count: 0, + }; // ------------------------------------------------------------------------- - // Factories + // Reducer // ------------------------------------------------------------------------- - static #reducer = makeAggregateReducer(User, "user"); - - static async getById(userId: string): Promise { - return eventStore.reduce({ stream: userId, reducer: this.#reducer }); - } - - // ------------------------------------------------------------------------- - // Folder - // ------------------------------------------------------------------------- - - with(event: EventRecord) { + with(event: Events["$events"][number]["$record"]) { switch (event.type) { - case "user:created": { - this.name = event.data.name; - this.email = event.data.email; + case "user:name:given-set": { + this.name.given = event.data; + break; + } + case "user:name:family-set": { + this.name.family = event.data; break; } 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; } } } // ------------------------------------------------------------------------- - // 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}`; } } @@ -120,6 +192,11 @@ type Name = { given: string; family: string; }; + +type UserPosts = { + list: string[]; + count: number; +}; ``` ### 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`. ```ts -import { projector } from "./event-store.ts"; +import { Projector } from "@valkyr/event-store"; -projector.on("user:created", async (record) => { - // do something with the event record ... +import store from "./event-store.ts"; +import type { Events } from "./events.ts"; + +const projector = new Projector(); + +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 }); ``` diff --git a/adapters/postgres/README.md b/adapters/postgres/README.md index 7190faa..393f4e9 100644 --- a/adapters/postgres/README.md +++ b/adapters/postgres/README.md @@ -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 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({ - connection: () => postgres("postgres://${string}:${string}@${string}:${number}/${string}"), // lazy loaded connection - schema: "event_store", +export const eventStore = new EventStore({ + adapter: new PostgresAdapter(connection, { schema: "event_store" }), events, - validators, - 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. - }, - }, + hooks, }); -const projector = new Projector(); +const projector = new Projector(); eventStore.onEventsInserted(async (records, { batch }) => { // trigger event side effects here such as sending the records through