@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.
- package/README.md +164 -0
- 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.
|
|
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.
|
|
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": "
|
|
33
|
+
"gitHead": "6c7724dea1b5eda89e83319dce69703d622aadb1"
|
|
34
34
|
}
|