@ossy/event-store 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +164 -0
  2. package/package.json +3 -3
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # @ossy/event-store
2
+
3
+ Event sourcing primitives for the Ossy platform. Provides `Aggregate` (stream interaction), `EventStore` (raw event I/O), and `AggregateRebuild` (startup snapshot rebuilding) on top of MongoDB.
4
+
5
+ ## Core concepts
6
+
7
+ The event store keeps two MongoDB collections:
8
+
9
+ - **`eventstore`** — every event ever appended. Immutable. Each document is one domain event with `aggregateId`, `aggregateType`, `aggregateVersion`, `type`, `payload`, `created`, and `createdBy`.
10
+ - **`aggregates`** — denormalized state snapshots, rebuilt from the event stream. Used for fast queries. Rebuilt automatically at startup and kept up-to-date as new events arrive.
11
+
12
+ ---
13
+
14
+ ## `Aggregate`
15
+
16
+ The main entry point for reading and writing event streams. All methods return Promises and are chainable with `.then()`.
17
+
18
+ ### Reading the current state
19
+
20
+ ```js
21
+ import { Aggregate } from '@ossy/event-store'
22
+ import { Order } from '@acme/orders'
23
+
24
+ // Fold all events for `orderId` through Order.View() and return the result
25
+ const state = await Aggregate.Of(Order, orderId).then(Aggregate.View())
26
+ ```
27
+
28
+ ### Creating a new aggregate
29
+
30
+ Pass an event object as the identifier. The event is appended and the new aggregate is returned.
31
+
32
+ ```js
33
+ const newOrderEvent = { type: 'OrderPlaced', createdBy: userId, payload: { ... } }
34
+ const orderView = await Aggregate.Of(Order, newOrderEvent).then(Aggregate.View())
35
+ ```
36
+
37
+ An `aggregateId` inside the event is used as the aggregate's id; if absent, a `nanoid()` is generated.
38
+
39
+ ### Appending an event to an existing aggregate
40
+
41
+ ```js
42
+ await Aggregate.Of(Order, orderId)
43
+ .then(Aggregate.Add({ type: 'OrderShipped', createdBy: userId, payload: { shippedAt: Date.now() } }))
44
+ .then(Aggregate.Save())
45
+ ```
46
+
47
+ ### API reference
48
+
49
+ | Method | Signature | Description |
50
+ |---|---|---|
51
+ | `Aggregate.Of(Root, id)` | `(class, string) => Promise<Aggregate>` | Load an existing aggregate from the event store. |
52
+ | `Aggregate.Of(Root, event)` | `(class, object) => Promise<Aggregate>` | Create a new aggregate by appending the first event. |
53
+ | `Aggregate.Add(event)` | `(event) => (aggregate) => Promise<Aggregate>` | Append an event to the aggregate. Chainable. |
54
+ | `Aggregate.Save()` | `() => (aggregate) => Promise<void>` | Persist the current state snapshot to the `aggregates` collection. |
55
+ | `Aggregate.View(fn?)` | `(fn?) => (aggregate) => state` | Call the aggregate's `View(events, savedState)` and return the result. Pass a custom view function to override. |
56
+ | `Aggregate.Validate(fn)` | `(fn) => (aggregate) => Promise<Aggregate>` | Run a validation function against `(events, savedState)`. Reject if the function throws. |
57
+ | `Aggregate.Find(id)` | `(string) => Promise<object \| null>` | Low-level: find the snapshot document in `aggregates` by id. |
58
+ | `Aggregate.Collection` | `Collection` | Direct access to the MongoDB `aggregates` collection. |
59
+
60
+ ---
61
+
62
+ ## Writing an aggregate class
63
+
64
+ An aggregate class is a pure reducer — no side effects, no I/O. It declares a `static AggregateType` string and a `static View(events, savedState)` function.
65
+
66
+ ```js
67
+ // src/order.aggregate.js (inside an installable package)
68
+ export class Order {
69
+ static AggregateType = 'Order'
70
+
71
+ /**
72
+ * Folds an array of events into the current state.
73
+ * @param {object[]} events - Event documents in ascending version order.
74
+ * @param {object} savedState - Last saved snapshot (can be empty object).
75
+ * @returns {object}
76
+ */
77
+ static View(events, savedState = {}) {
78
+ return events.reduce((state, event) => {
79
+ switch (event.type) {
80
+ case 'OrderPlaced':
81
+ return { ...state, id: event.aggregateId, status: 'pending', ...event.payload }
82
+ case 'OrderShipped':
83
+ return { ...state, status: 'shipped', shippedAt: event.created }
84
+ case 'OrderCancelled':
85
+ return { ...state, status: 'cancelled' }
86
+ default:
87
+ return state
88
+ }
89
+ }, savedState)
90
+ }
91
+ }
92
+
93
+ // Required exports for the *.aggregate.js primitive
94
+ export { Order as Aggregate }
95
+ export const id = 'Order' // must equal Order.AggregateType
96
+ ```
97
+
98
+ > Aggregate files must be in an **installable package** (a `node_modules/` package with `"ossy": { "src": "./src" }` in `package.json`). The platform never loads aggregates from the local app's `src/` to avoid duplicates.
99
+
100
+ ---
101
+
102
+ ## `EventStore`
103
+
104
+ Low-level access to the `eventstore` collection. Prefer using `Aggregate` for most work.
105
+
106
+ ```js
107
+ import { EventStore } from '@ossy/event-store'
108
+ ```
109
+
110
+ | Method | Signature | Description |
111
+ |---|---|---|
112
+ | `EventStore.AppendEvent(event)` | `(object) => Promise<object>` | Insert an event document. Rejects on failure. |
113
+ | `EventStore.GetEventStream({ aggregateId, fromVersion? })` | `=> Promise<object[]>` | Return all events for an aggregate id, optionally after a version. |
114
+ | `EventStore.GetEventStreams()` | `=> Promise<{ aggregateId, aggregateType }[]>` | Return all known `(aggregateId, aggregateType)` pairs. |
115
+ | `EventStore.FindEvent(query)` | `(MongoQuery) => Promise<object>` | Find a single event. Rejects when not found. |
116
+ | `EventStore.FindEvents(query)` | `(MongoQuery) => Promise<object[]>` | Find multiple events. Rejects when none found. |
117
+ | `EventStore.Aggregate(pipeline)` | `(Pipeline) => Promise<object[]>` | Run a MongoDB aggregation pipeline against the event store. |
118
+ | `EventStore.Collection` | `Collection` | Direct access to the MongoDB `eventstore` collection. |
119
+
120
+ ---
121
+
122
+ ## `AggregateRebuild`
123
+
124
+ Rebuilds state snapshots from scratch at startup. Called automatically by `@ossy/platform` — you rarely need to use this directly.
125
+
126
+ ```js
127
+ import { AggregateRebuild } from '@ossy/event-store'
128
+
129
+ // Register an aggregate class (done automatically by the platform)
130
+ AggregateRebuild.registerAggregate({ id: 'Order', Aggregate: Order })
131
+
132
+ // Rebuild and save every known aggregate of every registered type
133
+ await AggregateRebuild.BuildAndSaveAll()
134
+
135
+ // Rebuild a single aggregate
136
+ await AggregateRebuild.BuildAndSave('Order', orderId)
137
+ ```
138
+
139
+ ---
140
+
141
+ ## `*.aggregate.js` primitive
142
+
143
+ The platform auto-discovers aggregate files from installed packages. See [PRIMITIVES.md](../platform/PRIMITIVES.md#aggregate) for the full specification.
144
+
145
+ ---
146
+
147
+ ## MongoDB setup
148
+
149
+ Connect to MongoDB before using any `Aggregate` or `EventStore` methods:
150
+
151
+ ```js
152
+ import { Mongo } from '@ossy/event-store'
153
+
154
+ await Mongo.connect(process.env.DB_URL)
155
+ ```
156
+
157
+ The platform server calls `Mongo.connect` automatically when `DB_URL` is present in the environment.
158
+
159
+ Indexes recommended for the `eventstore` collection:
160
+
161
+ ```js
162
+ { aggregateId: 1, aggregateVersion: 1 } // stream queries
163
+ { aggregateType: 1 } // type-level queries
164
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/event-store",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Ossy Event Store — Aggregate, EventStore, and MongoDB client",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,12 +23,12 @@
23
23
  "author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
- "@ossy/observability": "^1.4.0",
26
+ "@ossy/observability": "^1.5.0",
27
27
  "mongodb": "^7.2.0",
28
28
  "nanoid": "^5.1.11"
29
29
  },
30
30
  "files": [
31
31
  "src"
32
32
  ],
33
- "gitHead": "24df2bde0d5d8794c5a82b9e802a2594ca73a5d9"
33
+ "gitHead": "6c7724dea1b5eda89e83319dce69703d622aadb1"
34
34
  }