@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +11 -11
  3. package/src/LocalPlatformRegistry.res +19 -0
  4. package/src/LocalPlatformRegistry.res.mjs +19 -1
  5. package/src/Platform.res +24 -2
  6. package/src/Platform.res.mjs +22 -2
  7. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +38 -0
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +29 -0
  9. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +87 -0
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +76 -0
  11. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +8 -1
  12. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +17 -4
  13. package/src/adapter/EventLog/EventLogStorage_InMemory.res +49 -2
  14. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +57 -21
  15. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +85 -0
  16. package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +50 -0
  17. package/src/adapter/EventLog/LocalEventLogStorage.res +3 -0
  18. package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +6 -2
  19. package/src/adapter/LocalBus.res +32 -5
  20. package/src/adapter/LocalBus.res.mjs +27 -7
  21. package/src/adapter/LocalEventLogRead.res +477 -0
  22. package/src/adapter/LocalEventLogRead.res.mjs +507 -0
  23. package/src/adapter/LocalEventTap.res +18 -80
  24. package/src/adapter/LocalEventTap.res.mjs +19 -108
  25. package/src/adapter/LocalFeed.res +433 -0
  26. package/src/adapter/LocalFeed.res.mjs +419 -0
  27. package/src/adapter/LocalLogFeed.res +217 -0
  28. package/src/adapter/LocalLogFeed.res.mjs +295 -0
  29. package/src/adapter/LocalPgEventLogRead.res +85 -0
  30. package/src/adapter/LocalPgEventLogRead.res.mjs +91 -0
  31. package/src/adapter/LocalPlatformObserve.res +456 -0
  32. package/src/adapter/LocalPlatformObserve.res.mjs +505 -0
  33. package/src/adapter/LocalTapLines.res +147 -0
  34. package/src/adapter/LocalTapLines.res.mjs +229 -0
  35. package/src/adapter/PlatformGraphQL_Server.res +36 -3
  36. package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
  37. package/src/reexport/CommandTopic.res.mjs +5 -2
  38. package/tests/adapter/GraphQLWsFixtures.mjs +46 -0
  39. package/tests/adapter/LocalEventLogReadTest.res +539 -0
  40. package/tests/adapter/LocalEventLogReadTest.res.mjs +466 -0
  41. package/tests/adapter/LocalFeedTest.res +314 -0
  42. package/tests/adapter/LocalFeedTest.res.mjs +329 -0
  43. package/tests/adapter/LocalPlatformObserveTest.res +165 -0
  44. package/tests/adapter/LocalPlatformObserveTest.res.mjs +183 -0
  45. package/tests/adapter/LocalTapLinesTest.res +182 -0
  46. package/tests/adapter/LocalTapLinesTest.res.mjs +242 -0
  47. package/tests/adapter/PlatformSubscriptionsTest.res +123 -0
  48. package/tests/adapter/PlatformSubscriptionsTest.res.mjs +132 -0
  49. package/tests/components/automationslice/AutomationSliceCallbackTest.res +80 -0
  50. package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +81 -0
  51. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +17 -0
  52. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +24 -0
  53. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +30 -0
  54. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +28 -0
  55. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +58 -0
  56. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +71 -19
  57. 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
+ })