@reventlessdev/reventless-aws 3.0.0-alpha.174 → 3.0.0-alpha.176
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 +21 -0
- package/package.json +10 -10
- package/src/Platform.res +115 -31
- package/src/Platform.res.mjs +217 -44
- package/src/adapter/DcbEventLog/DcbBackend.res +2 -2
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res +4 -1
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs +1 -1
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res +14 -4
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs +19 -14
- package/src/adapter/EventLog/EventLogBackend.res +82 -0
- package/src/adapter/EventLog/EventLogBackend.res.mjs +53 -0
- package/src/adapter/EventLog/EventLogStorage.res +15 -0
- package/src/adapter/EventLog/EventLogStorage.res.mjs +20 -1
- package/src/adapter/EventLog/EventLogStorage_Postgres.res +37 -0
- package/src/adapter/EventLog/EventLogStorage_Postgres.res.mjs +21 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +26 -11
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +15 -6
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +78 -7
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +30 -2
- package/src/adapter/Postgres/PgProjectionFeed.res +100 -0
- package/src/adapter/Postgres/PgProjectionFeed.res.mjs +54 -0
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +192 -0
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +141 -0
- package/src/adapter/QueryDb/QueryDbBackend.res +52 -0
- package/src/adapter/QueryDb/QueryDbBackend.res.mjs +44 -0
- package/src/adapter/QueryDb/QueryDbResolvers.res +48 -0
- package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +17 -1
- package/src/adapter/QueryDb/QueryDbStorage.res +44 -0
- package/src/adapter/QueryDb/QueryDbStorage.res.mjs +34 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +43 -0
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +21 -0
- package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res +19 -0
- package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs +15 -0
- package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res +12 -1
- package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs +2 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +83 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +46 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +83 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +48 -3
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +130 -5
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +66 -6
- package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +15 -10
- package/src/adapter/Runtime/ReadModelEntryPoint.mjs +32 -15
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +26 -12
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +138 -8
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +76 -11
- package/src/components/Aggregate_Builder_Single.res +12 -5
- package/src/components/Aggregate_Builder_Single.res.mjs +12 -6
- package/src/components/Aggregate_Builder_Single_Async.res +12 -5
- package/src/components/Aggregate_Builder_Single_Async.res.mjs +12 -6
- package/src/components/ReadModel_Builder_NoResolver.res.mjs +1 -1
- package/src/components/ReadModel_Builder_NoResolver_Stream.res +13 -4
- package/src/components/ReadModel_Builder_NoResolver_Stream.res.mjs +14 -9
- package/src/components/ReadModel_Builder_Single.res +14 -5
- package/src/components/ReadModel_Builder_Single.res.mjs +15 -12
- package/src/components/ReadModel_Builder_Single_Stream.res +14 -5
- package/src/components/ReadModel_Builder_Single_Stream.res.mjs +15 -12
- package/src/components/StateViewSlice_Builder.res +10 -4
- package/src/components/StateViewSlice_Builder.res.mjs +8 -10
- package/src/components/StateViewSlice_Builder_Stream.res +10 -4
- package/src/components/StateViewSlice_Builder_Stream.res.mjs +8 -10
- package/src/plugin/runtime/PluginRuntime_Builder.res +12 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +4 -0
- package/tests/EventCollectorChannelStreamRuntimeTest.res +55 -0
- package/tests/EventCollectorChannelStreamRuntimeTest.res.mjs +78 -0
- package/tests/PgChangeFeedRelay_IntegrationTest.res +259 -0
- package/tests/PgChangeFeedRelay_IntegrationTest.res.mjs +238 -0
- package/tests/PgPipeline_IntegrationTest.res +208 -0
- package/tests/PgPipeline_IntegrationTest.res.mjs +260 -0
- package/tests/PgQueryResolver_LambdaTest.res +206 -0
- package/tests/PgQueryResolver_LambdaTest.res.mjs +259 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Live-Postgres integration suite for the change-feed relay (B2.4).
|
|
2
|
+
//
|
|
3
|
+
// Skipped unless PG_URL is set, so the default `pnpm test` stays dependency-free.
|
|
4
|
+
// Run against a real database with e.g.:
|
|
5
|
+
// PG_URL=postgres://postgres:postgres@localhost:5432/postgres pnpm test
|
|
6
|
+
//
|
|
7
|
+
// Exercises the deployed propagation path end-to-end minus AWS: DCB events are
|
|
8
|
+
// appended to a real `dcb_event` log (classic events to `event_log`), then
|
|
9
|
+
// `PgChangeFeedRelay_Runtime.relayWithPool` / `relayClassicWithPool` drains the
|
|
10
|
+
// feed, transforms each event into the EventCollector `{id, meta, event}` body,
|
|
11
|
+
// and hands the batch to an injected `sendBatch` (standing in for the SQS
|
|
12
|
+
// SendMessage the entry point wires). Asserts the emitted bodies, the partition-tag
|
|
13
|
+
// `id` derivation, and the checkpoints (a second drain sees nothing new; per-log
|
|
14
|
+
// subscribers stay isolated).
|
|
15
|
+
|
|
16
|
+
open JestGlobals
|
|
17
|
+
open ReventlessCore
|
|
18
|
+
open Reventless
|
|
19
|
+
|
|
20
|
+
@val external processEnv: dict<string> = "process.env"
|
|
21
|
+
let opts: Pulumi.CustomResourceOptions.t = {}
|
|
22
|
+
|
|
23
|
+
let jsonObj = pairs => JSON.Encode.object(Dict.fromArray(pairs))
|
|
24
|
+
|
|
25
|
+
let stored = (eventType, tags, data): DcbEventLog_Adapter.rawStoredEvent => {
|
|
26
|
+
eventType,
|
|
27
|
+
data,
|
|
28
|
+
tags,
|
|
29
|
+
meta: ReventlessCore.Message.generateMeta(~service="relay-it"),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Collect every JSON body handed to sendBatch across all drain pages.
|
|
33
|
+
let capturingSendBatch = sink => async jsons => sink := sink.contents->Array.concat(jsons)
|
|
34
|
+
|
|
35
|
+
let idOf = json =>
|
|
36
|
+
json->JSON.Decode.object->Option.flatMap(o => o->Dict.get("id"))->Option.flatMap(JSON.Decode.string)
|
|
37
|
+
|
|
38
|
+
switch processEnv->Dict.get("PG_URL") {
|
|
39
|
+
| None =>
|
|
40
|
+
testSync("Postgres relay integration (skipped — set PG_URL to run)", () =>
|
|
41
|
+
expect(true)->toBe(true)
|
|
42
|
+
)
|
|
43
|
+
| Some(url) =>
|
|
44
|
+
let pool = ReventlessPostgres.PgDriver.makePool({connectionString: url})
|
|
45
|
+
|
|
46
|
+
let makeLog = name =>
|
|
47
|
+
ReventlessPostgres.DcbEventLogStorage_Postgres.makeStorage(
|
|
48
|
+
~pool,
|
|
49
|
+
~name,
|
|
50
|
+
~indexes=[],
|
|
51
|
+
~partitionTag=DcbTag.Simple({key: "orderId"}),
|
|
52
|
+
~opts,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
beforeAllAsync(async () => {
|
|
56
|
+
await ReventlessPostgres.PgSchema.ensureSchema(pool)
|
|
57
|
+
await ReventlessPostgres.PgSchema.truncateAll(pool)
|
|
58
|
+
})
|
|
59
|
+
afterAll(() => {
|
|
60
|
+
let _ = pool->ReventlessPostgres.PgDriver.endPool
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe("PgChangeFeedRelay end-to-end (append → drain → EventCollector bodies)", () => {
|
|
64
|
+
testPromise("relays each appended DCB event as an {id, meta, event} body", async () => {
|
|
65
|
+
let (_n, ops, _s) = makeLog("relay-basic")
|
|
66
|
+
let mk = i =>
|
|
67
|
+
stored("OrderPlaced", [{key: "orderId", value: "o-1"}], jsonObj([("i", JSON.Encode.int(i))]))
|
|
68
|
+
let _ = await ops.append([mk(0), mk(1), mk(2)])
|
|
69
|
+
|
|
70
|
+
let sink = ref([])
|
|
71
|
+
let processed = await PgChangeFeedRelay_Runtime.relayWithPool(
|
|
72
|
+
~pool,
|
|
73
|
+
~logName="relay-basic",
|
|
74
|
+
~subscriber="relay-it",
|
|
75
|
+
~sendBatch=capturingSendBatch(sink),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
expect(processed)->toBe(3)
|
|
79
|
+
expect(sink.contents->Array.length)->toBe(3)
|
|
80
|
+
|
|
81
|
+
// Every body carries the derived id, the event type, and the producing service.
|
|
82
|
+
let first = sink.contents->Array.getUnsafe(0)
|
|
83
|
+
expect(idOf(first))->toEqual(Some("orderId:o-1"))
|
|
84
|
+
let obj = first->JSON.Decode.object->Option.getOrThrow
|
|
85
|
+
expect(obj->Dict.get("event")->Option.getOrThrow->JSON.stringify->String.includes("OrderPlaced"))->toBe(true)
|
|
86
|
+
let metaObj = obj->Dict.get("meta")->Option.flatMap(JSON.Decode.object)->Option.getOrThrow
|
|
87
|
+
expect(metaObj->Dict.get("service"))->toEqual(Some(JSON.Encode.string("relay-it")))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
testPromise("checkpoints — a second drain from the same subscriber sees nothing new", async () => {
|
|
91
|
+
let (_n, ops, _s) = makeLog("relay-checkpoint")
|
|
92
|
+
let mk = i => stored("Fed", [{key: "orderId", value: "f"}], jsonObj([("i", JSON.Encode.int(i))]))
|
|
93
|
+
let _ = await ops.append([mk(0), mk(1)])
|
|
94
|
+
|
|
95
|
+
let sink = ref([])
|
|
96
|
+
let first = await PgChangeFeedRelay_Runtime.relayWithPool(
|
|
97
|
+
~pool,
|
|
98
|
+
~logName="relay-checkpoint",
|
|
99
|
+
~subscriber="relay-it",
|
|
100
|
+
~sendBatch=capturingSendBatch(sink),
|
|
101
|
+
)
|
|
102
|
+
expect(first)->toBe(2)
|
|
103
|
+
|
|
104
|
+
let second = await PgChangeFeedRelay_Runtime.relayWithPool(
|
|
105
|
+
~pool,
|
|
106
|
+
~logName="relay-checkpoint",
|
|
107
|
+
~subscriber="relay-it",
|
|
108
|
+
~sendBatch=capturingSendBatch(sink),
|
|
109
|
+
)
|
|
110
|
+
expect(second)->toBe(0)
|
|
111
|
+
// No extra bodies were emitted on the second pass.
|
|
112
|
+
expect(sink.contents->Array.length)->toBe(2)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
testPromise("classic: relays each appended event_log event as an {id, meta, event} body", async () => {
|
|
116
|
+
let (_n, ops, _s) =
|
|
117
|
+
ReventlessPostgres.EventLogStorage_Postgres.makeStorage(
|
|
118
|
+
~pool,
|
|
119
|
+
~name="ClassicRelayBasicEventLog",
|
|
120
|
+
~opts=(),
|
|
121
|
+
)
|
|
122
|
+
// The flat on-disk item the classic append stores verbatim — same shape the
|
|
123
|
+
// DynamoDB path puts (id, position, event, data, decomposed meta).
|
|
124
|
+
let flatItem = (~id, ~seq, ~eventType, ~data) =>
|
|
125
|
+
[
|
|
126
|
+
("id", JSON.Encode.string(id)),
|
|
127
|
+
("position", JSON.Encode.string(seq->Int.toString->String.padStart(9, "0"))),
|
|
128
|
+
("event", JSON.Encode.string(eventType)),
|
|
129
|
+
("data", data),
|
|
130
|
+
]
|
|
131
|
+
->Array.concat(
|
|
132
|
+
ReventlessCore.Message.generateMeta(~service="relay-it")->ReventlessCore.Message.decomposeMeta,
|
|
133
|
+
)
|
|
134
|
+
->Dict.fromArray
|
|
135
|
+
->JSON.Encode.object
|
|
136
|
+
let _ = await ops.append(
|
|
137
|
+
0,
|
|
138
|
+
"agg-1",
|
|
139
|
+
[
|
|
140
|
+
flatItem(~id="agg-1", ~seq=0, ~eventType="NameUpdated", ~data=jsonObj([("n", JSON.Encode.int(0))])),
|
|
141
|
+
flatItem(~id="agg-1", ~seq=1, ~eventType="NameUpdated", ~data=jsonObj([("n", JSON.Encode.int(1))])),
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
let sink = ref([])
|
|
146
|
+
let processed = await PgChangeFeedRelay_Runtime.relayClassicWithPool(
|
|
147
|
+
~pool,
|
|
148
|
+
~logName="ClassicRelayBasicEventLog",
|
|
149
|
+
~subscriber="relay-it:ClassicRelayBasicEventLog",
|
|
150
|
+
~sendBatch=capturingSendBatch(sink),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
expect(processed)->toBe(2)
|
|
154
|
+
expect(sink.contents->Array.length)->toBe(2)
|
|
155
|
+
let first = sink.contents->Array.getUnsafe(0)
|
|
156
|
+
expect(idOf(first))->toEqual(Some("agg-1"))
|
|
157
|
+
let obj = first->JSON.Decode.object->Option.getOrThrow
|
|
158
|
+
expect(
|
|
159
|
+
obj->Dict.get("event")->Option.getOrThrow->JSON.stringify->String.includes("NameUpdated"),
|
|
160
|
+
)->toBe(true)
|
|
161
|
+
let metaObj = obj->Dict.get("meta")->Option.flatMap(JSON.Decode.object)->Option.getOrThrow
|
|
162
|
+
expect(metaObj->Dict.get("service"))->toEqual(Some(JSON.Encode.string("relay-it")))
|
|
163
|
+
|
|
164
|
+
// Checkpointed: a second drain from the same subscriber sees nothing new.
|
|
165
|
+
let second = await PgChangeFeedRelay_Runtime.relayClassicWithPool(
|
|
166
|
+
~pool,
|
|
167
|
+
~logName="ClassicRelayBasicEventLog",
|
|
168
|
+
~subscriber="relay-it:ClassicRelayBasicEventLog",
|
|
169
|
+
~sendBatch=capturingSendBatch(sink),
|
|
170
|
+
)
|
|
171
|
+
expect(second)->toBe(0)
|
|
172
|
+
expect(sink.contents->Array.length)->toBe(2)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
testPromise("classic: per-log subscribers keep checkpoints isolated across logs", async () => {
|
|
176
|
+
// event_log_subscription keys by subscriber alone. With a SHARED subscriber,
|
|
177
|
+
// draining log A after log B's events were appended would save a cursor past
|
|
178
|
+
// B's rows and silently skip them — the clobber the per-log subscriber fixes.
|
|
179
|
+
let mkOps = name => {
|
|
180
|
+
let (_n, ops, _s) =
|
|
181
|
+
ReventlessPostgres.EventLogStorage_Postgres.makeStorage(~pool, ~name, ~opts=())
|
|
182
|
+
ops
|
|
183
|
+
}
|
|
184
|
+
let opsB = mkOps("ClassicRelayIsoBEventLog")
|
|
185
|
+
let opsA = mkOps("ClassicRelayIsoAEventLog")
|
|
186
|
+
let meta = () =>
|
|
187
|
+
ReventlessCore.Message.generateMeta(~service="relay-it")->ReventlessCore.Message.decomposeMeta
|
|
188
|
+
let item = (id, eventType) =>
|
|
189
|
+
[
|
|
190
|
+
("id", JSON.Encode.string(id)),
|
|
191
|
+
("position", JSON.Encode.string("000000000")),
|
|
192
|
+
("event", JSON.Encode.string(eventType)),
|
|
193
|
+
("data", jsonObj([])),
|
|
194
|
+
]
|
|
195
|
+
->Array.concat(meta())
|
|
196
|
+
->Dict.fromArray
|
|
197
|
+
->JSON.Encode.object
|
|
198
|
+
// B's event commits FIRST (lower global_seq), then A's.
|
|
199
|
+
let _ = await opsB.append(0, "b-1", [item("b-1", "BHappened")])
|
|
200
|
+
let _ = await opsA.append(0, "a-1", [item("a-1", "AHappened")])
|
|
201
|
+
|
|
202
|
+
// Drain A first — its checkpoint lands PAST B's rows in global_seq order.
|
|
203
|
+
let sinkA = ref([])
|
|
204
|
+
let drainedA = await PgChangeFeedRelay_Runtime.relayClassicWithPool(
|
|
205
|
+
~pool,
|
|
206
|
+
~logName="ClassicRelayIsoAEventLog",
|
|
207
|
+
~subscriber="relay-it:ClassicRelayIsoAEventLog",
|
|
208
|
+
~sendBatch=capturingSendBatch(sinkA),
|
|
209
|
+
)
|
|
210
|
+
expect(drainedA)->toBe(1)
|
|
211
|
+
|
|
212
|
+
// B still sees its event because its subscriber has its own checkpoint.
|
|
213
|
+
let sinkB = ref([])
|
|
214
|
+
let drainedB = await PgChangeFeedRelay_Runtime.relayClassicWithPool(
|
|
215
|
+
~pool,
|
|
216
|
+
~logName="ClassicRelayIsoBEventLog",
|
|
217
|
+
~subscriber="relay-it:ClassicRelayIsoBEventLog",
|
|
218
|
+
~sendBatch=capturingSendBatch(sinkB),
|
|
219
|
+
)
|
|
220
|
+
expect(drainedB)->toBe(1)
|
|
221
|
+
expect(idOf(sinkB.contents->Array.getUnsafe(0)))->toEqual(Some("b-1"))
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
testPromise("partitionTag pins the id to its tag on a multi-tag event", async () => {
|
|
225
|
+
let (_n, ops, _s) =
|
|
226
|
+
ReventlessPostgres.DcbEventLogStorage_Postgres.makeStorage(
|
|
227
|
+
~pool,
|
|
228
|
+
~name="relay-pt",
|
|
229
|
+
~indexes=[],
|
|
230
|
+
~partitionTag=DcbTag.Simple({key: "customerId"}),
|
|
231
|
+
~opts,
|
|
232
|
+
)
|
|
233
|
+
let ev =
|
|
234
|
+
stored(
|
|
235
|
+
"OrderPlaced",
|
|
236
|
+
[{key: "orderId", value: "o-9"}, {key: "customerId", value: "c-9"}],
|
|
237
|
+
jsonObj([("n", JSON.Encode.int(1))]),
|
|
238
|
+
)
|
|
239
|
+
let _ = await ops.append([ev])
|
|
240
|
+
|
|
241
|
+
// The relay receives the sury-encoded partition tag exactly as the builder emits it.
|
|
242
|
+
let partitionTagJson =
|
|
243
|
+
DcbTag.Simple({key: "customerId"})->S.reverseConvertToJsonOrThrow(
|
|
244
|
+
Reventless.DcbTag.derivedPartitionTagSchema,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
let sink = ref([])
|
|
248
|
+
let _ = await PgChangeFeedRelay_Runtime.relayWithPool(
|
|
249
|
+
~pool,
|
|
250
|
+
~logName="relay-pt",
|
|
251
|
+
~subscriber="relay-it",
|
|
252
|
+
~partitionTagJson,
|
|
253
|
+
~sendBatch=capturingSendBatch(sink),
|
|
254
|
+
)
|
|
255
|
+
// Without the tag the id would be "orderId:o-9" (first tag); the tag pins it.
|
|
256
|
+
expect(idOf(sink.contents->Array.getUnsafe(0)))->toEqual(Some("customerId:c-9"))
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
7
|
+
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
8
|
+
import * as PgDriver$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgDriver.res.mjs";
|
|
9
|
+
import * as PgSchema$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgSchema.res.mjs";
|
|
10
|
+
import * as PgChangeFeedRelay_Runtime$ReventlessAws from "../src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs";
|
|
11
|
+
import * as EventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/EventLogStorage_Postgres.res.mjs";
|
|
12
|
+
import * as DcbEventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/DcbEventLogStorage_Postgres.res.mjs";
|
|
13
|
+
|
|
14
|
+
let opts = {};
|
|
15
|
+
|
|
16
|
+
function jsonObj(pairs) {
|
|
17
|
+
return Object.fromEntries(pairs);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stored(eventType, tags, data) {
|
|
21
|
+
return {
|
|
22
|
+
eventType: eventType,
|
|
23
|
+
data: data,
|
|
24
|
+
tags: tags,
|
|
25
|
+
meta: Message$ReventlessCore.generateMeta("relay-it", undefined, undefined, undefined, undefined, undefined, undefined, undefined)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function capturingSendBatch(sink) {
|
|
30
|
+
return async jsons => {
|
|
31
|
+
sink.contents = sink.contents.concat(jsons);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function idOf(json) {
|
|
36
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o["id"]), Stdlib_JSON.Decode.string);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let url = process.env["PG_URL"];
|
|
40
|
+
|
|
41
|
+
if (url !== undefined) {
|
|
42
|
+
let pool = PgDriver$ReventlessPostgres.makePool({
|
|
43
|
+
connectionString: url
|
|
44
|
+
});
|
|
45
|
+
let makeLog = name => DcbEventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, name, [], {
|
|
46
|
+
TAG: "Simple",
|
|
47
|
+
_0: {
|
|
48
|
+
key: "orderId"
|
|
49
|
+
}
|
|
50
|
+
}, undefined, opts, undefined, undefined);
|
|
51
|
+
globalThis.beforeAll(async () => {
|
|
52
|
+
await PgSchema$ReventlessPostgres.ensureSchema(pool);
|
|
53
|
+
return await PgSchema$ReventlessPostgres.truncateAll(pool);
|
|
54
|
+
});
|
|
55
|
+
globalThis.afterAll(() => {
|
|
56
|
+
PgDriver$ReventlessPostgres.endPool(pool);
|
|
57
|
+
});
|
|
58
|
+
globalThis.describe("PgChangeFeedRelay end-to-end (append → drain → EventCollector bodies)", () => {
|
|
59
|
+
globalThis.test("relays each appended DCB event as an {id, meta, event} body", async () => {
|
|
60
|
+
let match = makeLog("relay-basic");
|
|
61
|
+
let mk = i => stored("OrderPlaced", [{
|
|
62
|
+
key: "orderId",
|
|
63
|
+
value: "o-1"
|
|
64
|
+
}], Object.fromEntries([[
|
|
65
|
+
"i",
|
|
66
|
+
i
|
|
67
|
+
]]));
|
|
68
|
+
await match[1].append([
|
|
69
|
+
mk(0),
|
|
70
|
+
mk(1),
|
|
71
|
+
mk(2)
|
|
72
|
+
], undefined);
|
|
73
|
+
let sink = {
|
|
74
|
+
contents: []
|
|
75
|
+
};
|
|
76
|
+
let processed = await PgChangeFeedRelay_Runtime$ReventlessAws.relayWithPool(pool, "relay-basic", "relay-it", undefined, capturingSendBatch(sink));
|
|
77
|
+
globalThis.expect(processed).toBe(3);
|
|
78
|
+
globalThis.expect(sink.contents.length).toBe(3);
|
|
79
|
+
let first = sink.contents[0];
|
|
80
|
+
globalThis.expect(idOf(first)).toEqual("orderId:o-1");
|
|
81
|
+
let obj = Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(first), undefined);
|
|
82
|
+
globalThis.expect(JSON.stringify(Stdlib_Option.getOrThrow(obj["event"], undefined)).includes("OrderPlaced")).toBe(true);
|
|
83
|
+
let metaObj = Stdlib_Option.getOrThrow(Stdlib_Option.flatMap(obj["meta"], Stdlib_JSON.Decode.object), undefined);
|
|
84
|
+
globalThis.expect(metaObj["service"]).toEqual("relay-it");
|
|
85
|
+
});
|
|
86
|
+
globalThis.test("checkpoints — a second drain from the same subscriber sees nothing new", async () => {
|
|
87
|
+
let match = makeLog("relay-checkpoint");
|
|
88
|
+
let mk = i => stored("Fed", [{
|
|
89
|
+
key: "orderId",
|
|
90
|
+
value: "f"
|
|
91
|
+
}], Object.fromEntries([[
|
|
92
|
+
"i",
|
|
93
|
+
i
|
|
94
|
+
]]));
|
|
95
|
+
await match[1].append([
|
|
96
|
+
mk(0),
|
|
97
|
+
mk(1)
|
|
98
|
+
], undefined);
|
|
99
|
+
let sink = {
|
|
100
|
+
contents: []
|
|
101
|
+
};
|
|
102
|
+
let first = await PgChangeFeedRelay_Runtime$ReventlessAws.relayWithPool(pool, "relay-checkpoint", "relay-it", undefined, capturingSendBatch(sink));
|
|
103
|
+
globalThis.expect(first).toBe(2);
|
|
104
|
+
let second = await PgChangeFeedRelay_Runtime$ReventlessAws.relayWithPool(pool, "relay-checkpoint", "relay-it", undefined, capturingSendBatch(sink));
|
|
105
|
+
globalThis.expect(second).toBe(0);
|
|
106
|
+
globalThis.expect(sink.contents.length).toBe(2);
|
|
107
|
+
});
|
|
108
|
+
globalThis.test("classic: relays each appended event_log event as an {id, meta, event} body", async () => {
|
|
109
|
+
let match = EventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, "ClassicRelayBasicEventLog", undefined, undefined);
|
|
110
|
+
let flatItem = (id, seq, eventType, data) => Object.fromEntries([
|
|
111
|
+
[
|
|
112
|
+
"id",
|
|
113
|
+
id
|
|
114
|
+
],
|
|
115
|
+
[
|
|
116
|
+
"position",
|
|
117
|
+
seq.toString().padStart(9, "0")
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
"event",
|
|
121
|
+
eventType
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
"data",
|
|
125
|
+
data
|
|
126
|
+
]
|
|
127
|
+
].concat(Message$ReventlessCore.decomposeMeta(Message$ReventlessCore.generateMeta("relay-it", undefined, undefined, undefined, undefined, undefined, undefined, undefined))));
|
|
128
|
+
await match[1].append(0, "agg-1", [
|
|
129
|
+
flatItem("agg-1", 0, "NameUpdated", Object.fromEntries([[
|
|
130
|
+
"n",
|
|
131
|
+
0
|
|
132
|
+
]])),
|
|
133
|
+
flatItem("agg-1", 1, "NameUpdated", Object.fromEntries([[
|
|
134
|
+
"n",
|
|
135
|
+
1
|
|
136
|
+
]]))
|
|
137
|
+
]);
|
|
138
|
+
let sink = {
|
|
139
|
+
contents: []
|
|
140
|
+
};
|
|
141
|
+
let processed = await PgChangeFeedRelay_Runtime$ReventlessAws.relayClassicWithPool(pool, "ClassicRelayBasicEventLog", "relay-it:ClassicRelayBasicEventLog", capturingSendBatch(sink));
|
|
142
|
+
globalThis.expect(processed).toBe(2);
|
|
143
|
+
globalThis.expect(sink.contents.length).toBe(2);
|
|
144
|
+
let first = sink.contents[0];
|
|
145
|
+
globalThis.expect(idOf(first)).toEqual("agg-1");
|
|
146
|
+
let obj = Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(first), undefined);
|
|
147
|
+
globalThis.expect(JSON.stringify(Stdlib_Option.getOrThrow(obj["event"], undefined)).includes("NameUpdated")).toBe(true);
|
|
148
|
+
let metaObj = Stdlib_Option.getOrThrow(Stdlib_Option.flatMap(obj["meta"], Stdlib_JSON.Decode.object), undefined);
|
|
149
|
+
globalThis.expect(metaObj["service"]).toEqual("relay-it");
|
|
150
|
+
let second = await PgChangeFeedRelay_Runtime$ReventlessAws.relayClassicWithPool(pool, "ClassicRelayBasicEventLog", "relay-it:ClassicRelayBasicEventLog", capturingSendBatch(sink));
|
|
151
|
+
globalThis.expect(second).toBe(0);
|
|
152
|
+
globalThis.expect(sink.contents.length).toBe(2);
|
|
153
|
+
});
|
|
154
|
+
globalThis.test("classic: per-log subscribers keep checkpoints isolated across logs", async () => {
|
|
155
|
+
let mkOps = name => EventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, name, undefined, undefined)[1];
|
|
156
|
+
let opsB = mkOps("ClassicRelayIsoBEventLog");
|
|
157
|
+
let opsA = mkOps("ClassicRelayIsoAEventLog");
|
|
158
|
+
let meta = () => Message$ReventlessCore.decomposeMeta(Message$ReventlessCore.generateMeta("relay-it", undefined, undefined, undefined, undefined, undefined, undefined, undefined));
|
|
159
|
+
let item = (id, eventType) => Object.fromEntries([
|
|
160
|
+
[
|
|
161
|
+
"id",
|
|
162
|
+
id
|
|
163
|
+
],
|
|
164
|
+
[
|
|
165
|
+
"position",
|
|
166
|
+
"000000000"
|
|
167
|
+
],
|
|
168
|
+
[
|
|
169
|
+
"event",
|
|
170
|
+
eventType
|
|
171
|
+
],
|
|
172
|
+
[
|
|
173
|
+
"data",
|
|
174
|
+
Object.fromEntries([])
|
|
175
|
+
]
|
|
176
|
+
].concat(meta()));
|
|
177
|
+
await opsB.append(0, "b-1", [item("b-1", "BHappened")]);
|
|
178
|
+
await opsA.append(0, "a-1", [item("a-1", "AHappened")]);
|
|
179
|
+
let sinkA = {
|
|
180
|
+
contents: []
|
|
181
|
+
};
|
|
182
|
+
let drainedA = await PgChangeFeedRelay_Runtime$ReventlessAws.relayClassicWithPool(pool, "ClassicRelayIsoAEventLog", "relay-it:ClassicRelayIsoAEventLog", capturingSendBatch(sinkA));
|
|
183
|
+
globalThis.expect(drainedA).toBe(1);
|
|
184
|
+
let sinkB = {
|
|
185
|
+
contents: []
|
|
186
|
+
};
|
|
187
|
+
let drainedB = await PgChangeFeedRelay_Runtime$ReventlessAws.relayClassicWithPool(pool, "ClassicRelayIsoBEventLog", "relay-it:ClassicRelayIsoBEventLog", capturingSendBatch(sinkB));
|
|
188
|
+
globalThis.expect(drainedB).toBe(1);
|
|
189
|
+
globalThis.expect(idOf(sinkB.contents[0])).toEqual("b-1");
|
|
190
|
+
});
|
|
191
|
+
globalThis.test("partitionTag pins the id to its tag on a multi-tag event", async () => {
|
|
192
|
+
let match = DcbEventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, "relay-pt", [], {
|
|
193
|
+
TAG: "Simple",
|
|
194
|
+
_0: {
|
|
195
|
+
key: "customerId"
|
|
196
|
+
}
|
|
197
|
+
}, undefined, opts, undefined, undefined);
|
|
198
|
+
let ev = stored("OrderPlaced", [
|
|
199
|
+
{
|
|
200
|
+
key: "orderId",
|
|
201
|
+
value: "o-9"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
key: "customerId",
|
|
205
|
+
value: "c-9"
|
|
206
|
+
}
|
|
207
|
+
], Object.fromEntries([[
|
|
208
|
+
"n",
|
|
209
|
+
1
|
|
210
|
+
]]));
|
|
211
|
+
await match[1].append([ev], undefined);
|
|
212
|
+
let partitionTagJson = S.reverseConvertToJsonOrThrow({
|
|
213
|
+
TAG: "Simple",
|
|
214
|
+
_0: {
|
|
215
|
+
key: "customerId"
|
|
216
|
+
}
|
|
217
|
+
}, DcbTag$Reventless.derivedPartitionTagSchema);
|
|
218
|
+
let sink = {
|
|
219
|
+
contents: []
|
|
220
|
+
};
|
|
221
|
+
await PgChangeFeedRelay_Runtime$ReventlessAws.relayWithPool(pool, "relay-pt", "relay-it", partitionTagJson, capturingSendBatch(sink));
|
|
222
|
+
globalThis.expect(idOf(sink.contents[0])).toEqual("customerId:c-9");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
} else {
|
|
226
|
+
globalThis.test("Postgres relay integration (skipped — set PG_URL to run)", () => {
|
|
227
|
+
globalThis.expect(true).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export {
|
|
232
|
+
opts,
|
|
233
|
+
jsonObj,
|
|
234
|
+
stored,
|
|
235
|
+
capturingSendBatch,
|
|
236
|
+
idOf,
|
|
237
|
+
}
|
|
238
|
+
/* url Not a pure module */
|