@ossy/event-store 1.0.1 → 1.2.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/package.json +3 -2
- package/src/aggregate-rebuild.js +10 -16
- package/src/aggregate.js +13 -20
- package/src/event-store.js +17 -27
- package/src/mongodb.js +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/event-store",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Ossy Event Store — Aggregate, EventStore, and MongoDB client",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,11 +23,12 @@
|
|
|
23
23
|
"author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"@ossy/observability": "^1.2.0",
|
|
26
27
|
"mongodb": "^7.2.0",
|
|
27
28
|
"nanoid": "^5.1.11"
|
|
28
29
|
},
|
|
29
30
|
"files": [
|
|
30
31
|
"src"
|
|
31
32
|
],
|
|
32
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "46cdd391ec01ea9210543ee7b14661f77079a7e7"
|
|
33
34
|
}
|
package/src/aggregate-rebuild.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import { Aggregate } from './aggregate.js'
|
|
2
2
|
import { EventStore } from './event-store.js'
|
|
3
|
+
import { createLogger } from '@ossy/observability'
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
function _log(message) { console.log(message) }
|
|
7
|
-
function _warn(message) { console.warn(message) }
|
|
8
|
-
function _logDebug(message) {
|
|
9
|
-
if (!_debug) return
|
|
10
|
-
console.log(message)
|
|
11
|
-
}
|
|
5
|
+
const log = createLogger('event-store')
|
|
12
6
|
|
|
13
7
|
export class AggregateRebuild {
|
|
14
8
|
|
|
@@ -25,18 +19,18 @@ export class AggregateRebuild {
|
|
|
25
19
|
static registerAggregate(mod) {
|
|
26
20
|
const { id, Aggregate: AggregateClass } = mod
|
|
27
21
|
if (typeof AggregateClass !== 'function' || !id) {
|
|
28
|
-
|
|
22
|
+
log.warn('[AggregateRebuild] registerAggregate: invalid module — expected { id, Aggregate }')
|
|
29
23
|
return
|
|
30
24
|
}
|
|
31
25
|
AggregateRebuild._aggregateMap[id] = AggregateClass
|
|
32
|
-
|
|
26
|
+
log.debug(`[AggregateRebuild] Registered aggregate: ${id}`)
|
|
33
27
|
}
|
|
34
28
|
|
|
35
29
|
static async BuildAndSave(aggregateType, aggregateId) {
|
|
36
30
|
const AggregateRoot = AggregateRebuild._aggregateMap[aggregateType]
|
|
37
31
|
|
|
38
32
|
if (!AggregateRoot) {
|
|
39
|
-
|
|
33
|
+
log.debug(`[AggregateRebuild][BuildAndSave] Skipping unregistered aggregate type: ${aggregateType}`)
|
|
40
34
|
return
|
|
41
35
|
}
|
|
42
36
|
|
|
@@ -45,12 +39,12 @@ export class AggregateRebuild {
|
|
|
45
39
|
}
|
|
46
40
|
|
|
47
41
|
static async BuildAndSaveAll() {
|
|
48
|
-
|
|
42
|
+
log.debug('[AggregateRebuild][BuildAndSaveAll] Starting building aggregates')
|
|
49
43
|
|
|
50
44
|
const knownTypes = new Set(Object.keys(AggregateRebuild._aggregateMap))
|
|
51
45
|
|
|
52
46
|
if (knownTypes.size === 0) {
|
|
53
|
-
|
|
47
|
+
log.warn('[AggregateRebuild][BuildAndSaveAll] No aggregates registered — skipping rebuild')
|
|
54
48
|
return
|
|
55
49
|
}
|
|
56
50
|
|
|
@@ -63,9 +57,9 @@ export class AggregateRebuild {
|
|
|
63
57
|
.then(results => {
|
|
64
58
|
const failed = results.filter(result => result.status === 'rejected')
|
|
65
59
|
const success = results.filter(result => result.status === 'fulfilled')
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
log.info('[AggregateRebuild][BuildAndSaveAll] Finished building aggregates')
|
|
61
|
+
log.info(`[AggregateRebuild][BuildAndSaveAll] Failed: ${failed.length}`)
|
|
62
|
+
log.info(`[AggregateRebuild][BuildAndSaveAll] Success: ${success.length}`)
|
|
69
63
|
})
|
|
70
64
|
}
|
|
71
65
|
}
|
package/src/aggregate.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid'
|
|
2
2
|
import { Mongo, withMongoReconnect } from './mongodb.js'
|
|
3
3
|
import { EventStore } from './event-store.js'
|
|
4
|
+
import { createLogger } from '@ossy/observability'
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
function _log(message) { console.log(message) }
|
|
8
|
-
function _logError(message) { console.error(message) }
|
|
9
|
-
function _logDebug(message) {
|
|
10
|
-
if (!_debug) return
|
|
11
|
-
console.log(message)
|
|
12
|
-
}
|
|
6
|
+
const log = createLogger('event-store')
|
|
13
7
|
|
|
14
8
|
/**
|
|
15
9
|
* Utility class that helps interact with event streams.
|
|
@@ -35,7 +29,7 @@ export class Aggregate {
|
|
|
35
29
|
*/
|
|
36
30
|
static Of(AggregateRoot, identifier) {
|
|
37
31
|
|
|
38
|
-
|
|
32
|
+
log.info(`[Aggregate][Of()][${AggregateRoot?.AggregateType}] Creating aggregate root`)
|
|
39
33
|
|
|
40
34
|
if (typeof identifier === 'string') {
|
|
41
35
|
|
|
@@ -47,20 +41,20 @@ export class Aggregate {
|
|
|
47
41
|
}
|
|
48
42
|
|
|
49
43
|
if (typeof identifier === 'object') {
|
|
50
|
-
|
|
44
|
+
log.info('[Aggregate] Creating new aggregate event stream')
|
|
51
45
|
return Aggregate.Add(identifier)(new Aggregate(AggregateRoot, identifier.aggregateId || nanoid(), []))
|
|
52
46
|
}
|
|
53
47
|
|
|
54
|
-
|
|
48
|
+
log.error('[Aggregate] No recognizable identifier provided')
|
|
55
49
|
return Promise.reject()
|
|
56
50
|
}
|
|
57
51
|
|
|
58
52
|
static Find(identifier) {
|
|
59
|
-
|
|
53
|
+
log.info(`[Aggregate][Find()] Fetching aggregate for ${identifier}`)
|
|
60
54
|
return withMongoReconnect(() => Aggregate.Collection.findOne({ id: identifier }))
|
|
61
55
|
.then(aggregate => {
|
|
62
56
|
if (!aggregate) {
|
|
63
|
-
|
|
57
|
+
log.debug(`[Aggregate][Find()] No aggregate found for ${identifier}`)
|
|
64
58
|
}
|
|
65
59
|
return aggregate
|
|
66
60
|
})
|
|
@@ -89,17 +83,17 @@ export class Aggregate {
|
|
|
89
83
|
|
|
90
84
|
static Save() {
|
|
91
85
|
return aggregate => {
|
|
92
|
-
|
|
86
|
+
log.debug(`[Aggregate][Save] Saving state for ${aggregate.type} ${aggregate.id}`)
|
|
93
87
|
|
|
94
88
|
if (!aggregate.events || aggregate.events.length === 0) {
|
|
95
|
-
|
|
89
|
+
log.debug(`[Aggregate][Save] No events to save for ${aggregate.type} ${aggregate.id}`)
|
|
96
90
|
return Promise.resolve()
|
|
97
91
|
}
|
|
98
92
|
|
|
99
93
|
const latestEventVersion = [...aggregate.events].sort((a, b) => a.aggregateVersion - b.aggregateVersion).pop()?.aggregateVersion
|
|
100
94
|
|
|
101
95
|
if (latestEventVersion < aggregate.version) {
|
|
102
|
-
|
|
96
|
+
log.debug(`[Aggregate][Save] No new events to save for ${aggregate.type} ${aggregate.id}`)
|
|
103
97
|
return Promise.resolve()
|
|
104
98
|
}
|
|
105
99
|
|
|
@@ -116,8 +110,7 @@ export class Aggregate {
|
|
|
116
110
|
}, { upsert: true })
|
|
117
111
|
)
|
|
118
112
|
.catch(error => {
|
|
119
|
-
|
|
120
|
-
console.error(error)
|
|
113
|
+
log.error(`[Aggregate][Save] Failed to save state for ${aggregate.type} ${aggregate.id}`, undefined, error)
|
|
121
114
|
})
|
|
122
115
|
}
|
|
123
116
|
}
|
|
@@ -130,7 +123,7 @@ export class Aggregate {
|
|
|
130
123
|
|
|
131
124
|
static View(view) {
|
|
132
125
|
return aggregate => {
|
|
133
|
-
|
|
126
|
+
log.debug(`[Aggregate][View] Building view for ${aggregate.type} ${aggregate.id}`)
|
|
134
127
|
|
|
135
128
|
if (typeof view === 'function') {
|
|
136
129
|
return view(aggregate.events, aggregate.state)
|
|
@@ -150,7 +143,7 @@ export class Aggregate {
|
|
|
150
143
|
* @param {Object} aggregate - The object representing the aggregate of all the events
|
|
151
144
|
*/
|
|
152
145
|
constructor(AggregateRoot, id, events, aggregate) {
|
|
153
|
-
|
|
146
|
+
log.debug(`[Aggregate][Constructor] Assembling ${AggregateRoot.AggregateType} ${id}`)
|
|
154
147
|
|
|
155
148
|
this.id = id;
|
|
156
149
|
this.version = aggregate?.version || 0
|
package/src/event-store.js
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
import { Mongo, withMongoReconnect } from './mongodb.js'
|
|
2
|
+
import { createLogger } from '@ossy/observability'
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
function _log(message) { console.log(message) }
|
|
6
|
-
function _logError(message, error) {
|
|
7
|
-
console.error(message)
|
|
8
|
-
if (error) console.error('[Reason]:', error)
|
|
9
|
-
}
|
|
10
|
-
function _logDebug(message, data) {
|
|
11
|
-
if (!_debug) return
|
|
12
|
-
console.log(message)
|
|
13
|
-
if (data !== undefined) console.log('[DEBUG DATA]:', data)
|
|
14
|
-
}
|
|
4
|
+
const log = createLogger('event-store')
|
|
15
5
|
|
|
16
6
|
/**
|
|
17
7
|
* Database queries related to workspace events
|
|
@@ -24,8 +14,8 @@ export class EventStore {
|
|
|
24
14
|
}
|
|
25
15
|
|
|
26
16
|
static AppendEvent(event) {
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
log.info('[EventStore] Appending event')
|
|
18
|
+
log.debug('[EventStore] Event', { event })
|
|
29
19
|
|
|
30
20
|
return withMongoReconnect(() => EventStore.Collection.insertOne(event))
|
|
31
21
|
.then(insertResult => {
|
|
@@ -34,33 +24,33 @@ export class EventStore {
|
|
|
34
24
|
: Promise.reject()
|
|
35
25
|
})
|
|
36
26
|
.catch(error => {
|
|
37
|
-
|
|
27
|
+
log.error('[EventStore] Could not append event', undefined, error)
|
|
38
28
|
return Promise.reject()
|
|
39
29
|
})
|
|
40
30
|
}
|
|
41
31
|
|
|
42
32
|
static FindEvent(query) {
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
log.info('[EventStore] Searching for event')
|
|
34
|
+
log.debug('[EventStore] Query', { query })
|
|
45
35
|
|
|
46
36
|
return withMongoReconnect(() => EventStore.Collection.findOne(query))
|
|
47
37
|
.then(event => !!event ? event : Promise.reject())
|
|
48
38
|
.catch(error => {
|
|
49
|
-
|
|
39
|
+
log.error('[EventStore] No event found', undefined, error)
|
|
50
40
|
return Promise.reject()
|
|
51
41
|
})
|
|
52
42
|
}
|
|
53
43
|
|
|
54
44
|
static FindEvents(query) {
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
log.info('[EventStore] Searching for events')
|
|
46
|
+
log.debug('[EventStore] Query', { query })
|
|
57
47
|
|
|
58
48
|
return withMongoReconnect(() =>
|
|
59
49
|
EventStore.Collection.find(query, { sort: { aggregateVersion: 1 } }).toArray()
|
|
60
50
|
)
|
|
61
51
|
.then(events => !!events?.length ? events : Promise.reject())
|
|
62
52
|
.catch(error => {
|
|
63
|
-
|
|
53
|
+
log.error('[EventStore] No events found', undefined, error)
|
|
64
54
|
return Promise.reject()
|
|
65
55
|
})
|
|
66
56
|
}
|
|
@@ -69,7 +59,7 @@ export class EventStore {
|
|
|
69
59
|
aggregateId,
|
|
70
60
|
fromVersion = 0
|
|
71
61
|
}) {
|
|
72
|
-
|
|
62
|
+
log.debug(`[EventStore][GetEventStream()] Fetching stream ${aggregateId} from version ${fromVersion}`)
|
|
73
63
|
|
|
74
64
|
const query = {
|
|
75
65
|
aggregateId: aggregateId,
|
|
@@ -82,13 +72,13 @@ export class EventStore {
|
|
|
82
72
|
|
|
83
73
|
return withMongoReconnect(() => EventStore.Collection.find(query, options).toArray())
|
|
84
74
|
.then(events => {
|
|
85
|
-
|
|
75
|
+
log.debug(`[EventStore][GetEventStream()] Found ${events.length} events for ${aggregateId}`)
|
|
86
76
|
return events
|
|
87
77
|
})
|
|
88
78
|
}
|
|
89
79
|
|
|
90
80
|
static GetEventStreams() {
|
|
91
|
-
|
|
81
|
+
log.info('[EventStore][GetEventStreams()] Fetching event streams')
|
|
92
82
|
|
|
93
83
|
return EventStore.Aggregate([
|
|
94
84
|
{
|
|
@@ -108,18 +98,18 @@ export class EventStore {
|
|
|
108
98
|
}
|
|
109
99
|
])
|
|
110
100
|
.then(result => {
|
|
111
|
-
|
|
101
|
+
log.debug(`[EventStore][GetEventStreams()] found ${result.length} streams`)
|
|
112
102
|
return result
|
|
113
103
|
})
|
|
114
104
|
}
|
|
115
105
|
|
|
116
106
|
static Aggregate(pipeline) {
|
|
117
|
-
|
|
107
|
+
log.info('[EventStore] Running aggregation pipeline')
|
|
118
108
|
|
|
119
109
|
return withMongoReconnect(() => EventStore.Collection.aggregate(pipeline).toArray())
|
|
120
110
|
.then(events => (Array.isArray(events) ? events : []))
|
|
121
111
|
.catch(error => {
|
|
122
|
-
|
|
112
|
+
log.error('[EventStore] Aggregation failed', undefined, error)
|
|
123
113
|
return Promise.reject(error)
|
|
124
114
|
})
|
|
125
115
|
}
|
package/src/mongodb.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRequire } from 'module'
|
|
2
2
|
import { MongoClient } from 'mongodb'
|
|
3
|
+
import { createLogger } from '@ossy/observability'
|
|
3
4
|
|
|
4
5
|
// When bundled as ESM by @ossy/app (Rollup + @rollup/plugin-commonjs), the
|
|
5
6
|
// MongoDB driver's resolveRuntimeAdapters references `require` as a free
|
|
@@ -9,6 +10,8 @@ if (!globalThis.require) {
|
|
|
9
10
|
globalThis.require = createRequire(import.meta.url)
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
const log = createLogger('event-store')
|
|
14
|
+
|
|
12
15
|
const MONGO_URL = process.env.DB_URL || 'mongodb://mongodb:27017/'
|
|
13
16
|
const DB_NAME = process.env.DB_NAME || 'test'
|
|
14
17
|
|
|
@@ -33,7 +36,7 @@ export function isMongoTopologyClosedError(error) {
|
|
|
33
36
|
export function withMongoReconnect(run) {
|
|
34
37
|
return run().catch((err) => {
|
|
35
38
|
if (isMongoTopologyClosedError(err)) {
|
|
36
|
-
|
|
39
|
+
log.info('[Mongo] Topology closed — resetting client and retrying once')
|
|
37
40
|
Mongo.resetClient()
|
|
38
41
|
return run()
|
|
39
42
|
}
|
|
@@ -59,7 +62,7 @@ export class Mongo {
|
|
|
59
62
|
if (prev) {
|
|
60
63
|
prev.close().catch(() => {})
|
|
61
64
|
}
|
|
62
|
-
|
|
65
|
+
log.info('[Mongo] New MongoClient instance created')
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
/** Used by tests / graceful shutdown; resets singleton so a later operation opens a new client. */
|