@reventlessdev/reventless-local 3.0.0-alpha.316 → 3.0.0-alpha.318
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/CHANGELOG.md +16 -0
- package/package.json +11 -11
- package/src/LocalPlatformRegistry.res +19 -0
- package/src/LocalPlatformRegistry.res.mjs +19 -1
- package/src/Platform.res +24 -2
- package/src/Platform.res.mjs +22 -2
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +38 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +29 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +87 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +76 -0
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +8 -1
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +17 -4
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +49 -2
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +57 -21
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +85 -0
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +50 -0
- package/src/adapter/EventLog/LocalEventLogStorage.res +3 -0
- package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +6 -2
- package/src/adapter/LocalBus.res +32 -5
- package/src/adapter/LocalBus.res.mjs +27 -7
- package/src/adapter/LocalEventLogRead.res +477 -0
- package/src/adapter/LocalEventLogRead.res.mjs +507 -0
- package/src/adapter/LocalEventTap.res +18 -80
- package/src/adapter/LocalEventTap.res.mjs +19 -108
- package/src/adapter/LocalFeed.res +433 -0
- package/src/adapter/LocalFeed.res.mjs +419 -0
- package/src/adapter/LocalLogFeed.res +217 -0
- package/src/adapter/LocalLogFeed.res.mjs +295 -0
- package/src/adapter/LocalPgEventLogRead.res +85 -0
- package/src/adapter/LocalPgEventLogRead.res.mjs +91 -0
- package/src/adapter/LocalPlatformObserve.res +456 -0
- package/src/adapter/LocalPlatformObserve.res.mjs +505 -0
- package/src/adapter/LocalTapLines.res +147 -0
- package/src/adapter/LocalTapLines.res.mjs +229 -0
- package/src/adapter/PlatformGraphQL_Server.res +36 -3
- package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
- package/src/reexport/CommandTopic.res.mjs +5 -2
- package/tests/adapter/GraphQLWsFixtures.mjs +46 -0
- package/tests/adapter/LocalEventLogReadTest.res +539 -0
- package/tests/adapter/LocalEventLogReadTest.res.mjs +466 -0
- package/tests/adapter/LocalFeedTest.res +314 -0
- package/tests/adapter/LocalFeedTest.res.mjs +329 -0
- package/tests/adapter/LocalPlatformObserveTest.res +165 -0
- package/tests/adapter/LocalPlatformObserveTest.res.mjs +183 -0
- package/tests/adapter/LocalTapLinesTest.res +182 -0
- package/tests/adapter/LocalTapLinesTest.res.mjs +242 -0
- package/tests/adapter/PlatformSubscriptionsTest.res +123 -0
- package/tests/adapter/PlatformSubscriptionsTest.res.mjs +132 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +80 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +81 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +17 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +24 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +30 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +28 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +58 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +71 -19
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePiiTest.res.mjs +7 -7
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
// One read across every event log, newest first, on each backend: two plugins,
|
|
2
|
+
// aggregate and DCB logs. Pages join without a gap or a double while events are
|
|
3
|
+
// appended, every filter narrows, a correlation filter uses its index, and the
|
|
4
|
+
// tap's log and position are the read's.
|
|
5
|
+
|
|
6
|
+
@@warning("-44")
|
|
7
|
+
|
|
8
|
+
open JestGlobals
|
|
9
|
+
|
|
10
|
+
let _ = TestRunner.setup()
|
|
11
|
+
let opts: Pulumi.CustomResourceOptions.t = {}
|
|
12
|
+
|
|
13
|
+
@val external processEnv: dict<string> = "process.env"
|
|
14
|
+
|
|
15
|
+
// What the test appended, to compare the read with.
|
|
16
|
+
type appended = {
|
|
17
|
+
log: string,
|
|
18
|
+
plugin: string,
|
|
19
|
+
msgId: string,
|
|
20
|
+
eventType: string,
|
|
21
|
+
entityId?: string,
|
|
22
|
+
tags: array<Reventless.DcbTag.tag>,
|
|
23
|
+
correlationId: string,
|
|
24
|
+
causationId: string,
|
|
25
|
+
user: string,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let ordersLog = "OrderAggrEventLog"
|
|
29
|
+
let customersLog = "CustomerAggrEventLog"
|
|
30
|
+
let catalogLog = "CatalogDcbEventLog"
|
|
31
|
+
|
|
32
|
+
type world = {
|
|
33
|
+
appendOrder: (
|
|
34
|
+
~id: string,
|
|
35
|
+
~eventType: string,
|
|
36
|
+
~correlationId: string,
|
|
37
|
+
~user: string,
|
|
38
|
+
) => promise<unit>,
|
|
39
|
+
appendCustomer: (~id: string, ~correlationId: string, ~user: string) => promise<unit>,
|
|
40
|
+
appendProduct: (
|
|
41
|
+
~productId: string,
|
|
42
|
+
~eventType: string,
|
|
43
|
+
~correlationId: string,
|
|
44
|
+
~user: string,
|
|
45
|
+
) => promise<unit>,
|
|
46
|
+
appended: array<appended>,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let resolveOrThrow = async (r: promise<result<'a, 'e>>) =>
|
|
50
|
+
switch await r {
|
|
51
|
+
| Ok(_) => ()
|
|
52
|
+
| Error(_) => JsError.throwWithMessage("append failed")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Storages through the platform's own dispatchers, so each backend registers
|
|
56
|
+
its reader the way the platform does. */
|
|
57
|
+
let makeWorld = async (): world => {
|
|
58
|
+
LocalEventLogRead.resetForTests()
|
|
59
|
+
module Bus = LocalBus.MakeSilent()
|
|
60
|
+
module Agg = LocalEventLogStorage.Make(Bus)
|
|
61
|
+
module Dcb = LocalDcbEventLogStorage.Make(Bus)
|
|
62
|
+
let orders = await Agg.make(~name=ordersLog, ~owner=None, ~opts).operations->TestRunner.resolve
|
|
63
|
+
let customers = await Agg.make(
|
|
64
|
+
~name=customersLog,
|
|
65
|
+
~owner=None,
|
|
66
|
+
~opts,
|
|
67
|
+
).operations->TestRunner.resolve
|
|
68
|
+
let catalog = await Dcb.make(
|
|
69
|
+
~name=catalogLog,
|
|
70
|
+
~indexes=[],
|
|
71
|
+
~partitionTag=Reventless.DcbTag.Simple({key: "productId"}),
|
|
72
|
+
~opts,
|
|
73
|
+
).operations->TestRunner.resolve
|
|
74
|
+
LocalEventLogRead.notePlugin(~log=ordersLog, ~plugin="Ordering")
|
|
75
|
+
LocalEventLogRead.notePlugin(~log=customersLog, ~plugin="Ordering")
|
|
76
|
+
LocalEventLogRead.notePlugin(~log=catalogLog, ~plugin="Catalog")
|
|
77
|
+
|
|
78
|
+
let appended = []
|
|
79
|
+
let seqs = Dict.make()
|
|
80
|
+
let flat = (~id, ~eventType, ~meta: Reventless.Message.meta) =>
|
|
81
|
+
Dict.fromArray([
|
|
82
|
+
("id", JSON.Encode.string(id)),
|
|
83
|
+
("position", JSON.Encode.string("000000000")),
|
|
84
|
+
("event", JSON.Encode.string(eventType)),
|
|
85
|
+
("data", Dict.fromArray([("TAG", JSON.Encode.string(eventType))])->JSON.Encode.object),
|
|
86
|
+
("recordedAt", JSON.Encode.string(Date.make()->Date.toISOString)),
|
|
87
|
+
])
|
|
88
|
+
->Dict.toArray
|
|
89
|
+
->Array.concat(ReventlessCore.Message.decomposeMeta(meta))
|
|
90
|
+
->Dict.fromArray
|
|
91
|
+
->JSON.Encode.object
|
|
92
|
+
let appendAggregate = async (
|
|
93
|
+
ops: ReventlessCore.EventLog_Adapter.operations,
|
|
94
|
+
~log,
|
|
95
|
+
~plugin,
|
|
96
|
+
~id,
|
|
97
|
+
~eventType,
|
|
98
|
+
~correlationId,
|
|
99
|
+
~user,
|
|
100
|
+
) => {
|
|
101
|
+
let meta = ReventlessCore.Message.generateMeta(
|
|
102
|
+
~service="test",
|
|
103
|
+
~user,
|
|
104
|
+
~correlationId,
|
|
105
|
+
~causationId=`cause-of-${correlationId}`,
|
|
106
|
+
)
|
|
107
|
+
let key = log ++ "/" ++ id
|
|
108
|
+
let seq = seqs->Dict.get(key)->Option.getOr(0)
|
|
109
|
+
await ops.append(seq, id, [flat(~id, ~eventType, ~meta)])->resolveOrThrow
|
|
110
|
+
seqs->Dict.set(key, seq + 1)
|
|
111
|
+
appended->Array.push({
|
|
112
|
+
log,
|
|
113
|
+
plugin,
|
|
114
|
+
msgId: meta.msgId,
|
|
115
|
+
eventType,
|
|
116
|
+
entityId: id,
|
|
117
|
+
tags: [],
|
|
118
|
+
correlationId,
|
|
119
|
+
causationId: `cause-of-${correlationId}`,
|
|
120
|
+
user,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
{
|
|
124
|
+
appendOrder: (~id, ~eventType, ~correlationId, ~user) =>
|
|
125
|
+
appendAggregate(
|
|
126
|
+
orders,
|
|
127
|
+
~log=ordersLog,
|
|
128
|
+
~plugin="Ordering",
|
|
129
|
+
~id,
|
|
130
|
+
~eventType,
|
|
131
|
+
~correlationId,
|
|
132
|
+
~user,
|
|
133
|
+
),
|
|
134
|
+
appendCustomer: (~id, ~correlationId, ~user) =>
|
|
135
|
+
appendAggregate(
|
|
136
|
+
customers,
|
|
137
|
+
~log=customersLog,
|
|
138
|
+
~plugin="Ordering",
|
|
139
|
+
~id,
|
|
140
|
+
~eventType="Registered",
|
|
141
|
+
~correlationId,
|
|
142
|
+
~user,
|
|
143
|
+
),
|
|
144
|
+
appendProduct: async (~productId, ~eventType, ~correlationId, ~user) => {
|
|
145
|
+
let meta = ReventlessCore.Message.generateMeta(
|
|
146
|
+
~service="test",
|
|
147
|
+
~user,
|
|
148
|
+
~correlationId,
|
|
149
|
+
~causationId=`cause-of-${correlationId}`,
|
|
150
|
+
)
|
|
151
|
+
let tags: array<Reventless.DcbTag.tag> = [{key: "productId", value: productId}]
|
|
152
|
+
await catalog.append([
|
|
153
|
+
{
|
|
154
|
+
eventType,
|
|
155
|
+
data: Dict.fromArray([("TAG", JSON.Encode.string(eventType))])->JSON.Encode.object,
|
|
156
|
+
tags,
|
|
157
|
+
meta,
|
|
158
|
+
},
|
|
159
|
+
])->resolveOrThrow
|
|
160
|
+
appended->Array.push({
|
|
161
|
+
log: catalogLog,
|
|
162
|
+
plugin: "Catalog",
|
|
163
|
+
msgId: meta.msgId,
|
|
164
|
+
eventType,
|
|
165
|
+
tags,
|
|
166
|
+
correlationId,
|
|
167
|
+
causationId: `cause-of-${correlationId}`,
|
|
168
|
+
user,
|
|
169
|
+
})
|
|
170
|
+
},
|
|
171
|
+
appended,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let seed = async (w: world) =>
|
|
176
|
+
for i in 1 to 12 {
|
|
177
|
+
let correlationId = mod(i, 3) == 0 ? "flow-a" : "flow-b"
|
|
178
|
+
let user = mod(i, 2) == 0 ? "u-even" : "u-odd"
|
|
179
|
+
await w.appendOrder(
|
|
180
|
+
~id=`o-${mod(i, 3)->Int.toString}`,
|
|
181
|
+
~eventType=mod(i, 2) == 0 ? "Placed" : "Paid",
|
|
182
|
+
~correlationId,
|
|
183
|
+
~user,
|
|
184
|
+
)
|
|
185
|
+
await w.appendProduct(
|
|
186
|
+
~productId=`p-${mod(i, 2)->Int.toString}`,
|
|
187
|
+
~eventType=mod(i, 4) == 0 ? "Archived" : "Added",
|
|
188
|
+
~correlationId,
|
|
189
|
+
~user,
|
|
190
|
+
)
|
|
191
|
+
if mod(i, 4) == 1 {
|
|
192
|
+
await w.appendCustomer(~id=`c-${i->Int.toString}`, ~correlationId, ~user)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let msgIdOf = (r: LocalEventLogRead.record) =>
|
|
197
|
+
LocalEventLogRead.metaString(r.meta, "msgId")->Option.getOr("")
|
|
198
|
+
|
|
199
|
+
let readAll = async (~filter: LocalEventLogRead.filter, ~first: int): array<
|
|
200
|
+
LocalEventLogRead.record,
|
|
201
|
+
> => {
|
|
202
|
+
let out = []
|
|
203
|
+
let after = ref(None)
|
|
204
|
+
let more = ref(true)
|
|
205
|
+
while more.contents {
|
|
206
|
+
let page = await LocalEventLogRead.read(~filter, ~first, ~after=?after.contents)
|
|
207
|
+
page.edges->Array.forEach(((r, _)) => out->Array.push(r))
|
|
208
|
+
more := page.hasNextPage
|
|
209
|
+
after := page.endCursor
|
|
210
|
+
}
|
|
211
|
+
out
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let sorted = (ids: array<string>) => ids->Array.toSorted(String.compare)
|
|
215
|
+
|
|
216
|
+
let scenario = (~name: string, ~setup: unit => promise<unit>, ~teardown: unit => promise<unit>) =>
|
|
217
|
+
describe(`LocalEventLogRead on ${name}`, () => {
|
|
218
|
+
let run = async (body: unit => promise<unit>) => {
|
|
219
|
+
await setup()
|
|
220
|
+
let outcome = try {
|
|
221
|
+
await body()
|
|
222
|
+
Ok()
|
|
223
|
+
} catch {
|
|
224
|
+
| e => Error(e)
|
|
225
|
+
}
|
|
226
|
+
await teardown()
|
|
227
|
+
switch outcome {
|
|
228
|
+
| Ok() => ()
|
|
229
|
+
| Error(e) => throw(e)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
test(
|
|
234
|
+
"pages run newest first and join without a gap or a double while events are appended",
|
|
235
|
+
() =>
|
|
236
|
+
run(
|
|
237
|
+
async () => {
|
|
238
|
+
let w = await makeWorld()
|
|
239
|
+
await seed(w)
|
|
240
|
+
let total = w.appended->Array.length
|
|
241
|
+
let everything = await readAll(~filter={}, ~first=1000)
|
|
242
|
+
expect(everything->Array.length)->toBe(total)
|
|
243
|
+
expect(everything->Array.map(msgIdOf)->sorted)->toEqual(
|
|
244
|
+
w.appended->Array.map(a => a.msgId)->sorted,
|
|
245
|
+
)
|
|
246
|
+
// Newest first by the time each event was recorded.
|
|
247
|
+
everything->Array.forEachWithIndex(
|
|
248
|
+
(r, i) =>
|
|
249
|
+
switch everything->Array.get(i + 1) {
|
|
250
|
+
| Some(next) => expect(r.recordedAt >= next.recordedAt)->toBe(true)
|
|
251
|
+
| None => ()
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
expect(everything->Array.some(r => r.plugin == Some("Catalog")))->toBe(true)
|
|
255
|
+
expect(everything->Array.some(r => r.plugin == Some("Ordering")))->toBe(true)
|
|
256
|
+
|
|
257
|
+
// Paged by five, with events appended to every log after the first page.
|
|
258
|
+
let first = await LocalEventLogRead.read(~filter={}, ~first=5)
|
|
259
|
+
expect(first.hasNextPage)->toBe(true)
|
|
260
|
+
await w.appendOrder(~id="o-new", ~eventType="Placed", ~correlationId="late", ~user="u")
|
|
261
|
+
await w.appendProduct(
|
|
262
|
+
~productId="p-new",
|
|
263
|
+
~eventType="Added",
|
|
264
|
+
~correlationId="late",
|
|
265
|
+
~user="u",
|
|
266
|
+
)
|
|
267
|
+
await w.appendCustomer(~id="c-new", ~correlationId="late", ~user="u")
|
|
268
|
+
let rest = []
|
|
269
|
+
let after = ref(first.endCursor)
|
|
270
|
+
let more = ref(true)
|
|
271
|
+
while more.contents {
|
|
272
|
+
let page = await LocalEventLogRead.read(~filter={}, ~first=5, ~after=?after.contents)
|
|
273
|
+
page.edges->Array.forEach(((r, _)) => rest->Array.push(r))
|
|
274
|
+
more := page.hasNextPage
|
|
275
|
+
after := page.endCursor
|
|
276
|
+
}
|
|
277
|
+
let paged = first.edges->Array.map(((r, _)) => r)->Array.concat(rest)
|
|
278
|
+
// The same events in the same order as one read before the appends.
|
|
279
|
+
expect(paged->Array.map(msgIdOf))->toEqual(everything->Array.map(msgIdOf))
|
|
280
|
+
|
|
281
|
+
// An edge's cursor continues right after it.
|
|
282
|
+
let (_, thirdCursor) = first.edges->Array.getUnsafe(2)
|
|
283
|
+
let fromThird = await LocalEventLogRead.read(~filter={}, ~first=2, ~after=thirdCursor)
|
|
284
|
+
expect(fromThird.edges->Array.map(((r, _)) => msgIdOf(r)))->toEqual(
|
|
285
|
+
everything->Array.slice(~start=3, ~end=5)->Array.map(msgIdOf),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
// A new read sees the late events first.
|
|
289
|
+
let fresh = await LocalEventLogRead.read(~filter={}, ~first=3)
|
|
290
|
+
expect(
|
|
291
|
+
fresh.edges->Array.every(
|
|
292
|
+
((r, _)) => LocalEventLogRead.metaString(r.meta, "correlationId") == Some("late"),
|
|
293
|
+
),
|
|
294
|
+
)->toBe(true)
|
|
295
|
+
},
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
test("every filter narrows", () =>
|
|
300
|
+
run(
|
|
301
|
+
async () => {
|
|
302
|
+
let w = await makeWorld()
|
|
303
|
+
await seed(w)
|
|
304
|
+
let expectFilter = async (filter: LocalEventLogRead.filter, keep: appended => bool) => {
|
|
305
|
+
let got = await readAll(~filter, ~first=4)
|
|
306
|
+
let wanted = w.appended->Array.filter(keep)->Array.map(a => a.msgId)->sorted
|
|
307
|
+
expect(wanted->Array.length > 0)->toBe(true)
|
|
308
|
+
expect(wanted->Array.length < w.appended->Array.length)->toBe(true)
|
|
309
|
+
expect(got->Array.map(msgIdOf)->sorted)->toEqual(wanted)
|
|
310
|
+
}
|
|
311
|
+
await expectFilter({plugins: ["Catalog"]}, a => a.plugin == "Catalog")
|
|
312
|
+
await expectFilter({logs: [customersLog, catalogLog]}, a => a.log != ordersLog)
|
|
313
|
+
await expectFilter(
|
|
314
|
+
{eventTypes: ["Placed", "Archived"]},
|
|
315
|
+
a => a.eventType == "Placed" || a.eventType == "Archived",
|
|
316
|
+
)
|
|
317
|
+
await expectFilter({entityId: "o-1"}, a => a.entityId == Some("o-1"))
|
|
318
|
+
await expectFilter({entityId: "p-1"}, a => a.tags->Array.some(t => t.value == "p-1"))
|
|
319
|
+
await expectFilter(
|
|
320
|
+
{tagKey: "productId", tagValue: "p-0"},
|
|
321
|
+
a => a.tags->Array.some(t => t.key == "productId" && t.value == "p-0"),
|
|
322
|
+
)
|
|
323
|
+
let one = w.appended->Array.getUnsafe(5)
|
|
324
|
+
await expectFilter({msgId: one.msgId}, a => a.msgId == one.msgId)
|
|
325
|
+
await expectFilter({correlationId: "flow-a"}, a => a.correlationId == "flow-a")
|
|
326
|
+
await expectFilter(
|
|
327
|
+
{causationId: "cause-of-flow-a"},
|
|
328
|
+
a => a.causationId == "cause-of-flow-a",
|
|
329
|
+
)
|
|
330
|
+
await expectFilter({user: "u-even"}, a => a.user == "u-even")
|
|
331
|
+
await expectFilter(
|
|
332
|
+
{plugins: ["Ordering"], correlationId: "flow-b", user: "u-odd"},
|
|
333
|
+
a => a.plugin == "Ordering" && a.correlationId == "flow-b" && a.user == "u-odd",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
// Time: the newer half of a read, by its own recordedAt.
|
|
337
|
+
let everything = await readAll(~filter={}, ~first=100)
|
|
338
|
+
let middle = (everything->Array.getUnsafe(everything->Array.length / 2)).recordedAt
|
|
339
|
+
let newer = await readAll(~filter={timeFrom: middle}, ~first=7)
|
|
340
|
+
expect(newer->Array.map(msgIdOf)->sorted)->toEqual(
|
|
341
|
+
everything->Array.filter(r => r.recordedAt >= middle)->Array.map(msgIdOf)->sorted,
|
|
342
|
+
)
|
|
343
|
+
let older = await readAll(~filter={timeTo: middle}, ~first=7)
|
|
344
|
+
expect(older->Array.map(msgIdOf)->sorted)->toEqual(
|
|
345
|
+
everything->Array.filter(r => r.recordedAt <= middle)->Array.map(msgIdOf)->sorted,
|
|
346
|
+
)
|
|
347
|
+
},
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
test("each event's log and position, as noted for the tap, are the read's", () =>
|
|
352
|
+
run(
|
|
353
|
+
async () => {
|
|
354
|
+
let w = await makeWorld()
|
|
355
|
+
await w.appendOrder(~id="o-1", ~eventType="Placed", ~correlationId="tap", ~user="u")
|
|
356
|
+
await w.appendProduct(
|
|
357
|
+
~productId="p-1",
|
|
358
|
+
~eventType="Added",
|
|
359
|
+
~correlationId="tap",
|
|
360
|
+
~user="u",
|
|
361
|
+
)
|
|
362
|
+
let read = await readAll(~filter={}, ~first=10)
|
|
363
|
+
read->Array.forEach(
|
|
364
|
+
r => {
|
|
365
|
+
let noted = LocalEventLogRead.take(msgIdOf(r))
|
|
366
|
+
expect(
|
|
367
|
+
noted->Option.map(
|
|
368
|
+
((log, position)) => (log, position->LocalEventLogRead.positionToString),
|
|
369
|
+
),
|
|
370
|
+
)->toEqual(Some((r.log, r.position)))
|
|
371
|
+
},
|
|
372
|
+
)
|
|
373
|
+
},
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
scenario(~name="memory", ~setup=async () => BackendState.setMemory(), ~teardown=async () => ())
|
|
379
|
+
|
|
380
|
+
let sqliteDb: ref<option<SqliteDriver.t>> = ref(None)
|
|
381
|
+
scenario(
|
|
382
|
+
~name="SQLite",
|
|
383
|
+
~setup=async () => {
|
|
384
|
+
let db = SqliteDriver.openDb(~path=":memory:")
|
|
385
|
+
sqliteDb := Some(db)
|
|
386
|
+
BackendState.setSqlite(~db, ~path=":memory:")
|
|
387
|
+
},
|
|
388
|
+
~teardown=async () => {
|
|
389
|
+
BackendState.setMemory()
|
|
390
|
+
sqliteDb.contents->Option.forEach(SqliteDriver.close)
|
|
391
|
+
sqliteDb := None
|
|
392
|
+
},
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
switch processEnv->Dict.get("PG_URL") {
|
|
396
|
+
| None =>
|
|
397
|
+
testSync("LocalEventLogRead on Postgres (skipped — set PG_URL to run)", () =>
|
|
398
|
+
expect(true)->toBe(true)
|
|
399
|
+
)
|
|
400
|
+
| Some(url) =>
|
|
401
|
+
let pool = ReventlessPostgres.PgDriver.makePool({connectionString: url})
|
|
402
|
+
scenario(
|
|
403
|
+
~name="Postgres",
|
|
404
|
+
~setup=async () => {
|
|
405
|
+
await ReventlessPostgres.PgSchema.ensureSchema(pool)
|
|
406
|
+
await ReventlessPostgres.PgSchema.truncateAll(pool)
|
|
407
|
+
BackendState.setPostgres(~pool)
|
|
408
|
+
},
|
|
409
|
+
~teardown=async () => BackendState.setMemory(),
|
|
410
|
+
)
|
|
411
|
+
describe("the correlation index on Postgres", () => {
|
|
412
|
+
afterAll(() => {
|
|
413
|
+
let _ = pool->ReventlessPostgres.PgDriver.endPool
|
|
414
|
+
})
|
|
415
|
+
test("a correlation filter's plan uses the index, on both tables", async () => {
|
|
416
|
+
await ReventlessPostgres.PgSchema.ensureSchema(pool)
|
|
417
|
+
let plan = async sql =>
|
|
418
|
+
await pool->ReventlessPostgres.PgDriver.transaction(
|
|
419
|
+
async client => {
|
|
420
|
+
await client->ReventlessPostgres.PgDriver.execOn("SET LOCAL enable_seqscan = off")
|
|
421
|
+
(await client->ReventlessPostgres.PgDriver.queryOn(sql, []))
|
|
422
|
+
->Array.map(
|
|
423
|
+
row => row->Dict.valuesToArray->Array.map(v => JSON.stringify(v))->Array.join(" "),
|
|
424
|
+
)
|
|
425
|
+
->Array.join("\n")
|
|
426
|
+
},
|
|
427
|
+
)
|
|
428
|
+
let aggregate = await plan(
|
|
429
|
+
"EXPLAIN SELECT global_seq FROM event_log WHERE log_name = 'L' AND (payload->>'correlationId') = 'c' ORDER BY global_seq DESC LIMIT 10",
|
|
430
|
+
)
|
|
431
|
+
expect(aggregate->String.includes("event_log_correlation"))->toBe(true)
|
|
432
|
+
let dcb = await plan(
|
|
433
|
+
"EXPLAIN SELECT position FROM dcb_event WHERE log_name = 'L' AND (meta->>'correlationId') = 'c' ORDER BY position DESC LIMIT 10",
|
|
434
|
+
)
|
|
435
|
+
expect(dcb->String.includes("dcb_event_correlation"))->toBe(true)
|
|
436
|
+
})
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
describe("the correlation index on SQLite", () => {
|
|
441
|
+
let planOf = (db, (sql, params)) =>
|
|
442
|
+
db
|
|
443
|
+
->SqliteDriver.prepare("EXPLAIN QUERY PLAN " ++ sql)
|
|
444
|
+
->SqliteDriver.all(params)
|
|
445
|
+
->Array.map(row =>
|
|
446
|
+
row->Dict.get("detail")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
447
|
+
)
|
|
448
|
+
->Array.join("\n")
|
|
449
|
+
|
|
450
|
+
testSync("a correlation filter's plan uses the index, on both tables", () => {
|
|
451
|
+
let db = SqliteDriver.openDb(~path=":memory:")
|
|
452
|
+
EventLogStorage_Sqlite.ensureSchema(db)
|
|
453
|
+
DcbEventLogStorage_Sqlite.ensureSchema(db)
|
|
454
|
+
let aggregate = planOf(
|
|
455
|
+
db,
|
|
456
|
+
EventLogStorage_Sqlite.readBackSql(
|
|
457
|
+
~name="L",
|
|
458
|
+
~bound=Some(Below(100.)),
|
|
459
|
+
~limit=10,
|
|
460
|
+
~correlationId=Some("c"),
|
|
461
|
+
),
|
|
462
|
+
)
|
|
463
|
+
expect(aggregate->String.includes("event_log_by_correlation"))->toBe(true)
|
|
464
|
+
let dcb = planOf(
|
|
465
|
+
db,
|
|
466
|
+
DcbEventLogStorage_Sqlite.readBackSql(
|
|
467
|
+
~name="L",
|
|
468
|
+
~bound=Some(Below(100.)),
|
|
469
|
+
~limit=10,
|
|
470
|
+
~correlationId=Some("c"),
|
|
471
|
+
),
|
|
472
|
+
)
|
|
473
|
+
expect(dcb->String.includes("dcb_event_by_correlation"))->toBe(true)
|
|
474
|
+
db->SqliteDriver.close
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
testSync("an existing store gains the indexes when it is opened", () => {
|
|
478
|
+
let db = SqliteDriver.openDb(~path=":memory:")
|
|
479
|
+
db->SqliteDriver.exec(
|
|
480
|
+
"CREATE TABLE event_log (log_name TEXT NOT NULL, aggregate_id TEXT NOT NULL, seq_nr INTEGER NOT NULL, payload TEXT NOT NULL, PRIMARY KEY (log_name, aggregate_id, seq_nr))",
|
|
481
|
+
)
|
|
482
|
+
EventLogStorage_Sqlite.ensureSchema(db)
|
|
483
|
+
let names =
|
|
484
|
+
db
|
|
485
|
+
->SqliteDriver.prepare("SELECT name FROM sqlite_master WHERE type = 'index'")
|
|
486
|
+
->SqliteDriver.all([])
|
|
487
|
+
->Array.filterMap(row => row->Dict.get("name")->Option.flatMap(JSON.Decode.string))
|
|
488
|
+
expect(names->Array.includes("event_log_by_correlation"))->toBe(true)
|
|
489
|
+
db->SqliteDriver.close
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
type consoleObj = {mutable log: string => unit}
|
|
494
|
+
@val external console: consoleObj = "console"
|
|
495
|
+
|
|
496
|
+
describe("the tap line", () => {
|
|
497
|
+
testPromise("carries the event's log and position when its storage noted them", async () => {
|
|
498
|
+
NodeProcess.env->Dict.set("REVENTLESS_EVENT_TAP", "ndjson")
|
|
499
|
+
module Bus = LocalBus.MakeSilent()
|
|
500
|
+
let meta = ReventlessCore.Message.generateMeta(~service="test")
|
|
501
|
+
LocalEventLogRead.note(~log=ordersLog, ~msgId=meta.msgId, ~position=42.)
|
|
502
|
+
let printed = []
|
|
503
|
+
let original = console.log
|
|
504
|
+
console.log = s => printed->Array.push(s)
|
|
505
|
+
await Bus.publishEvent(
|
|
506
|
+
"OrderAggrEventTopic",
|
|
507
|
+
"Order",
|
|
508
|
+
meta,
|
|
509
|
+
JSON.parseOrThrow(`{"event":{"TAG":"Placed"}}`),
|
|
510
|
+
)
|
|
511
|
+
let other = ReventlessCore.Message.generateMeta(~service="test")
|
|
512
|
+
await Bus.publishEvent("Lonely", "svc", other, JSON.Encode.null)
|
|
513
|
+
console.log = original
|
|
514
|
+
NodeProcess.env->Dict.delete("REVENTLESS_EVENT_TAP")
|
|
515
|
+
let lines =
|
|
516
|
+
printed
|
|
517
|
+
->Array.filter(l => l->String.startsWith("@@RVLESS_EVT@@ "))
|
|
518
|
+
->Array.map(
|
|
519
|
+
l =>
|
|
520
|
+
l
|
|
521
|
+
->String.slice(~start=15, ~end=l->String.length)
|
|
522
|
+
->JSON.parseOrThrow
|
|
523
|
+
->JSON.Decode.object
|
|
524
|
+
->Option.getOrThrow,
|
|
525
|
+
)
|
|
526
|
+
let field = (d, k) => d->Dict.get(k)->Option.flatMap(JSON.Decode.string)
|
|
527
|
+
let first = lines->Array.getUnsafe(0)
|
|
528
|
+
expect(field(first, "log"))->toEqual(Some(ordersLog))
|
|
529
|
+
expect(field(first, "position"))->toEqual(Some("42"))
|
|
530
|
+
// Every field the line had before is still there.
|
|
531
|
+
["event", "seq", "topic", "service", "payload", "ts"]->Array.forEach(
|
|
532
|
+
k => expect(first->Dict.get(k)->Option.isSome)->toBe(true),
|
|
533
|
+
)
|
|
534
|
+
// An event published without a log has neither.
|
|
535
|
+
let second = lines->Array.getUnsafe(1)
|
|
536
|
+
expect(second->Dict.get("log"))->toEqual(None)
|
|
537
|
+
expect(second->Dict.get("position"))->toEqual(None)
|
|
538
|
+
})
|
|
539
|
+
})
|