@reventlessdev/reventless-aws 3.0.0-alpha.173 → 3.0.0-alpha.175
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 +17 -0
- package/package.json +6 -6
- package/src/Platform.res +77 -7
- package/src/Platform.res.mjs +77 -3
- package/src/adapter/DcbEventLog/DcbBackend.res +78 -0
- package/src/adapter/DcbEventLog/DcbBackend.res.mjs +53 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage.res +20 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage.res.mjs +21 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +43 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res.mjs +21 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +7 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +7 -1
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +33 -8
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +12 -3
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +84 -6
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +49 -4
- package/src/components/Api/AppSync_Adapter.res +3 -3
- package/src/components/Api/AppSync_Adapter.res.mjs +2 -2
- package/src/components/Plugin.res +1 -1
- package/src/components/Plugin.res.mjs +2 -2
- package/src/plugin/runtime/PluginRuntime_Builder.res +20 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +8 -0
- package/tests/AppSync_AdapterTest.res +12 -12
- package/tests/AppSync_AdapterTest.res.mjs +15 -15
- package/tests/PgChangeFeedRelay_IntegrationTest.res +148 -0
- package/tests/PgChangeFeedRelay_IntegrationTest.res.mjs +154 -0
- package/tests/PgChangeFeedRelay_RuntimeTest.res +31 -0
- package/tests/PgChangeFeedRelay_RuntimeTest.res.mjs +65 -0
|
@@ -282,14 +282,14 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
282
282
|
});
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
-
globalThis.describe("AppSync_Adapter.injectAwsAuth —
|
|
285
|
+
globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
|
|
286
286
|
let makeFragment = (mutationFields, queryFields) => GraphQL_Stitcher$ReventlessCore.encode({
|
|
287
287
|
types: [],
|
|
288
288
|
mutations: mutationFields,
|
|
289
289
|
queries: queryFields,
|
|
290
290
|
subscriptions: []
|
|
291
291
|
});
|
|
292
|
-
globalThis.test("
|
|
292
|
+
globalThis.test("systemCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
|
|
293
293
|
let entry_fieldNames = ["p_Sync"];
|
|
294
294
|
let entry_fieldPermissions = Object.fromEntries([[
|
|
295
295
|
"p_Sync",
|
|
@@ -298,12 +298,12 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", (
|
|
|
298
298
|
_0: ["Admin"]
|
|
299
299
|
}
|
|
300
300
|
]]);
|
|
301
|
-
let
|
|
301
|
+
let entry_systemCallable = true;
|
|
302
302
|
let entry = {
|
|
303
303
|
fieldNames: entry_fieldNames,
|
|
304
304
|
commandSchema: S.unknown,
|
|
305
305
|
fieldPermissions: entry_fieldPermissions,
|
|
306
|
-
|
|
306
|
+
systemCallable: entry_systemCallable
|
|
307
307
|
};
|
|
308
308
|
let frag = makeFragment(["p_Sync(id: ID!): String"], []);
|
|
309
309
|
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
@@ -312,18 +312,18 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", (
|
|
|
312
312
|
globalThis.expect(m).toContain("@aws_iam");
|
|
313
313
|
globalThis.expect(m).not.toContain("@aws_auth");
|
|
314
314
|
});
|
|
315
|
-
globalThis.test("
|
|
315
|
+
globalThis.test("systemCallable mutation without a group restriction stays open to Cognito + IAM", () => {
|
|
316
316
|
let entry_fieldNames = ["p_Sync"];
|
|
317
317
|
let entry_fieldPermissions = Object.fromEntries([[
|
|
318
318
|
"p_Sync",
|
|
319
319
|
"AllowAuthenticated"
|
|
320
320
|
]]);
|
|
321
|
-
let
|
|
321
|
+
let entry_systemCallable = true;
|
|
322
322
|
let entry = {
|
|
323
323
|
fieldNames: entry_fieldNames,
|
|
324
324
|
commandSchema: S.unknown,
|
|
325
325
|
fieldPermissions: entry_fieldPermissions,
|
|
326
|
-
|
|
326
|
+
systemCallable: entry_systemCallable
|
|
327
327
|
};
|
|
328
328
|
let frag = makeFragment(["p_Sync(id: ID!): String"], []);
|
|
329
329
|
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
@@ -332,7 +332,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", (
|
|
|
332
332
|
globalThis.expect(m).not.toContain("cognito_groups");
|
|
333
333
|
globalThis.expect(m).not.toContain("@aws_auth");
|
|
334
334
|
});
|
|
335
|
-
globalThis.test("a non-
|
|
335
|
+
globalThis.test("a non-systemCallable sibling keeps single-mode @aws_auth (no @aws_iam)", () => {
|
|
336
336
|
let entry_fieldNames = [
|
|
337
337
|
"p_Sync",
|
|
338
338
|
"p_Other"
|
|
@@ -353,12 +353,12 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", (
|
|
|
353
353
|
}
|
|
354
354
|
]
|
|
355
355
|
]);
|
|
356
|
-
let
|
|
356
|
+
let entry_systemCallable = false;
|
|
357
357
|
let entry = {
|
|
358
358
|
fieldNames: entry_fieldNames,
|
|
359
359
|
commandSchema: S.unknown,
|
|
360
360
|
fieldPermissions: entry_fieldPermissions,
|
|
361
|
-
|
|
361
|
+
systemCallable: entry_systemCallable
|
|
362
362
|
};
|
|
363
363
|
let iamEntry_fieldNames = ["p_Sync"];
|
|
364
364
|
let iamEntry_fieldPermissions = Object.fromEntries([[
|
|
@@ -368,12 +368,12 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", (
|
|
|
368
368
|
_0: ["Admin"]
|
|
369
369
|
}
|
|
370
370
|
]]);
|
|
371
|
-
let
|
|
371
|
+
let iamEntry_systemCallable = true;
|
|
372
372
|
let iamEntry = {
|
|
373
373
|
fieldNames: iamEntry_fieldNames,
|
|
374
374
|
commandSchema: S.unknown,
|
|
375
375
|
fieldPermissions: iamEntry_fieldPermissions,
|
|
376
|
-
|
|
376
|
+
systemCallable: iamEntry_systemCallable
|
|
377
377
|
};
|
|
378
378
|
let frag = makeFragment([
|
|
379
379
|
"p_Sync(id: ID!): String",
|
|
@@ -393,12 +393,12 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", (
|
|
|
393
393
|
return Stdlib_JsError.throwWithMessage("missing p_Other field");
|
|
394
394
|
}
|
|
395
395
|
});
|
|
396
|
-
globalThis.test("
|
|
396
|
+
globalThis.test("systemCallable query applies dual-auth to BOTH single and list fields", () => {
|
|
397
397
|
let entry_permission = {
|
|
398
398
|
TAG: "AllowGroups",
|
|
399
399
|
_0: ["Admin"]
|
|
400
400
|
};
|
|
401
|
-
let
|
|
401
|
+
let entry_systemCallable = true;
|
|
402
402
|
let entry = {
|
|
403
403
|
singleFieldName: "p_Item",
|
|
404
404
|
listFieldName: "p_Items",
|
|
@@ -406,7 +406,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", (
|
|
|
406
406
|
stateSchema: S.unknown,
|
|
407
407
|
authorization: undefined,
|
|
408
408
|
permission: entry_permission,
|
|
409
|
-
|
|
409
|
+
systemCallable: entry_systemCallable
|
|
410
410
|
};
|
|
411
411
|
let frag = makeFragment([], [
|
|
412
412
|
"p_Item(id: ID!): Item",
|
|
@@ -0,0 +1,148 @@
|
|
|
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, then `PgChangeFeedRelay_Runtime.relayWithPool`
|
|
9
|
+
// drains the feed, transforms each event into the EventCollector `{id, meta, event}`
|
|
10
|
+
// body, and hands the batch to an injected `sendBatch` (standing in for the SQS
|
|
11
|
+
// SendMessage the entry point wires). Asserts the emitted bodies, the partition-tag
|
|
12
|
+
// `id` derivation, and the checkpoint (a second drain sees nothing new).
|
|
13
|
+
|
|
14
|
+
open JestGlobals
|
|
15
|
+
open ReventlessCore
|
|
16
|
+
open Reventless
|
|
17
|
+
|
|
18
|
+
@val external processEnv: dict<string> = "process.env"
|
|
19
|
+
let opts: Pulumi.CustomResourceOptions.t = {}
|
|
20
|
+
|
|
21
|
+
let jsonObj = pairs => JSON.Encode.object(Dict.fromArray(pairs))
|
|
22
|
+
|
|
23
|
+
let stored = (eventType, tags, data): DcbEventLog_Adapter.rawStoredEvent => {
|
|
24
|
+
eventType,
|
|
25
|
+
data,
|
|
26
|
+
tags,
|
|
27
|
+
meta: ReventlessCore.Message.generateMeta(~service="relay-it"),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Collect every JSON body handed to sendBatch across all drain pages.
|
|
31
|
+
let capturingSendBatch = sink => async jsons => sink := sink.contents->Array.concat(jsons)
|
|
32
|
+
|
|
33
|
+
let idOf = json =>
|
|
34
|
+
json->JSON.Decode.object->Option.flatMap(o => o->Dict.get("id"))->Option.flatMap(JSON.Decode.string)
|
|
35
|
+
|
|
36
|
+
switch processEnv->Dict.get("PG_URL") {
|
|
37
|
+
| None =>
|
|
38
|
+
testSync("Postgres relay integration (skipped — set PG_URL to run)", () =>
|
|
39
|
+
expect(true)->toBe(true)
|
|
40
|
+
)
|
|
41
|
+
| Some(url) =>
|
|
42
|
+
let pool = ReventlessPostgres.PgDriver.makePool({connectionString: url})
|
|
43
|
+
|
|
44
|
+
let makeLog = name =>
|
|
45
|
+
ReventlessPostgres.DcbEventLogStorage_Postgres.makeStorage(
|
|
46
|
+
~pool,
|
|
47
|
+
~name,
|
|
48
|
+
~indexes=[],
|
|
49
|
+
~partitionTag=DcbTag.Simple({key: "orderId"}),
|
|
50
|
+
~opts,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
beforeAllAsync(async () => {
|
|
54
|
+
await ReventlessPostgres.PgSchema.ensureSchema(pool)
|
|
55
|
+
await ReventlessPostgres.PgSchema.truncateAll(pool)
|
|
56
|
+
})
|
|
57
|
+
afterAll(() => {
|
|
58
|
+
let _ = pool->ReventlessPostgres.PgDriver.endPool
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe("PgChangeFeedRelay end-to-end (append → drain → EventCollector bodies)", () => {
|
|
62
|
+
testPromise("relays each appended DCB event as an {id, meta, event} body", async () => {
|
|
63
|
+
let (_n, ops, _s) = makeLog("relay-basic")
|
|
64
|
+
let mk = i =>
|
|
65
|
+
stored("OrderPlaced", [{key: "orderId", value: "o-1"}], jsonObj([("i", JSON.Encode.int(i))]))
|
|
66
|
+
let _ = await ops.append([mk(0), mk(1), mk(2)])
|
|
67
|
+
|
|
68
|
+
let sink = ref([])
|
|
69
|
+
let processed = await PgChangeFeedRelay_Runtime.relayWithPool(
|
|
70
|
+
~pool,
|
|
71
|
+
~logName="relay-basic",
|
|
72
|
+
~subscriber="relay-it",
|
|
73
|
+
~sendBatch=capturingSendBatch(sink),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
expect(processed)->toBe(3)
|
|
77
|
+
expect(sink.contents->Array.length)->toBe(3)
|
|
78
|
+
|
|
79
|
+
// Every body carries the derived id, the event type, and the producing service.
|
|
80
|
+
let first = sink.contents->Array.getUnsafe(0)
|
|
81
|
+
expect(idOf(first))->toEqual(Some("orderId:o-1"))
|
|
82
|
+
let obj = first->JSON.Decode.object->Option.getOrThrow
|
|
83
|
+
expect(obj->Dict.get("event")->Option.getOrThrow->JSON.stringify->String.includes("OrderPlaced"))->toBe(true)
|
|
84
|
+
let metaObj = obj->Dict.get("meta")->Option.flatMap(JSON.Decode.object)->Option.getOrThrow
|
|
85
|
+
expect(metaObj->Dict.get("service"))->toEqual(Some(JSON.Encode.string("relay-it")))
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
testPromise("checkpoints — a second drain from the same subscriber sees nothing new", async () => {
|
|
89
|
+
let (_n, ops, _s) = makeLog("relay-checkpoint")
|
|
90
|
+
let mk = i => stored("Fed", [{key: "orderId", value: "f"}], jsonObj([("i", JSON.Encode.int(i))]))
|
|
91
|
+
let _ = await ops.append([mk(0), mk(1)])
|
|
92
|
+
|
|
93
|
+
let sink = ref([])
|
|
94
|
+
let first = await PgChangeFeedRelay_Runtime.relayWithPool(
|
|
95
|
+
~pool,
|
|
96
|
+
~logName="relay-checkpoint",
|
|
97
|
+
~subscriber="relay-it",
|
|
98
|
+
~sendBatch=capturingSendBatch(sink),
|
|
99
|
+
)
|
|
100
|
+
expect(first)->toBe(2)
|
|
101
|
+
|
|
102
|
+
let second = await PgChangeFeedRelay_Runtime.relayWithPool(
|
|
103
|
+
~pool,
|
|
104
|
+
~logName="relay-checkpoint",
|
|
105
|
+
~subscriber="relay-it",
|
|
106
|
+
~sendBatch=capturingSendBatch(sink),
|
|
107
|
+
)
|
|
108
|
+
expect(second)->toBe(0)
|
|
109
|
+
// No extra bodies were emitted on the second pass.
|
|
110
|
+
expect(sink.contents->Array.length)->toBe(2)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
testPromise("partitionTag pins the id to its tag on a multi-tag event", async () => {
|
|
114
|
+
let (_n, ops, _s) =
|
|
115
|
+
ReventlessPostgres.DcbEventLogStorage_Postgres.makeStorage(
|
|
116
|
+
~pool,
|
|
117
|
+
~name="relay-pt",
|
|
118
|
+
~indexes=[],
|
|
119
|
+
~partitionTag=DcbTag.Simple({key: "customerId"}),
|
|
120
|
+
~opts,
|
|
121
|
+
)
|
|
122
|
+
let ev =
|
|
123
|
+
stored(
|
|
124
|
+
"OrderPlaced",
|
|
125
|
+
[{key: "orderId", value: "o-9"}, {key: "customerId", value: "c-9"}],
|
|
126
|
+
jsonObj([("n", JSON.Encode.int(1))]),
|
|
127
|
+
)
|
|
128
|
+
let _ = await ops.append([ev])
|
|
129
|
+
|
|
130
|
+
// The relay receives the sury-encoded partition tag exactly as the builder emits it.
|
|
131
|
+
let partitionTagJson =
|
|
132
|
+
DcbTag.Simple({key: "customerId"})->S.reverseConvertToJsonOrThrow(
|
|
133
|
+
Reventless.DcbTag.derivedPartitionTagSchema,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
let sink = ref([])
|
|
137
|
+
let _ = await PgChangeFeedRelay_Runtime.relayWithPool(
|
|
138
|
+
~pool,
|
|
139
|
+
~logName="relay-pt",
|
|
140
|
+
~subscriber="relay-it",
|
|
141
|
+
~partitionTagJson,
|
|
142
|
+
~sendBatch=capturingSendBatch(sink),
|
|
143
|
+
)
|
|
144
|
+
// Without the tag the id would be "orderId:o-9" (first tag); the tag pins it.
|
|
145
|
+
expect(idOf(sink.contents->Array.getUnsafe(0)))->toEqual(Some("customerId:c-9"))
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
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 DcbEventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/DcbEventLogStorage_Postgres.res.mjs";
|
|
12
|
+
|
|
13
|
+
let opts = {};
|
|
14
|
+
|
|
15
|
+
function jsonObj(pairs) {
|
|
16
|
+
return Object.fromEntries(pairs);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function stored(eventType, tags, data) {
|
|
20
|
+
return {
|
|
21
|
+
eventType: eventType,
|
|
22
|
+
data: data,
|
|
23
|
+
tags: tags,
|
|
24
|
+
meta: Message$ReventlessCore.generateMeta("relay-it", undefined, undefined, undefined, undefined, undefined, undefined, undefined)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function capturingSendBatch(sink) {
|
|
29
|
+
return async jsons => {
|
|
30
|
+
sink.contents = sink.contents.concat(jsons);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function idOf(json) {
|
|
35
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o["id"]), Stdlib_JSON.Decode.string);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let url = process.env["PG_URL"];
|
|
39
|
+
|
|
40
|
+
if (url !== undefined) {
|
|
41
|
+
let pool = PgDriver$ReventlessPostgres.makePool({
|
|
42
|
+
connectionString: url
|
|
43
|
+
});
|
|
44
|
+
let makeLog = name => DcbEventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, name, [], {
|
|
45
|
+
TAG: "Simple",
|
|
46
|
+
_0: {
|
|
47
|
+
key: "orderId"
|
|
48
|
+
}
|
|
49
|
+
}, undefined, opts, undefined, undefined);
|
|
50
|
+
globalThis.beforeAll(async () => {
|
|
51
|
+
await PgSchema$ReventlessPostgres.ensureSchema(pool);
|
|
52
|
+
return await PgSchema$ReventlessPostgres.truncateAll(pool);
|
|
53
|
+
});
|
|
54
|
+
globalThis.afterAll(() => {
|
|
55
|
+
PgDriver$ReventlessPostgres.endPool(pool);
|
|
56
|
+
});
|
|
57
|
+
globalThis.describe("PgChangeFeedRelay end-to-end (append → drain → EventCollector bodies)", () => {
|
|
58
|
+
globalThis.test("relays each appended DCB event as an {id, meta, event} body", async () => {
|
|
59
|
+
let match = makeLog("relay-basic");
|
|
60
|
+
let mk = i => stored("OrderPlaced", [{
|
|
61
|
+
key: "orderId",
|
|
62
|
+
value: "o-1"
|
|
63
|
+
}], Object.fromEntries([[
|
|
64
|
+
"i",
|
|
65
|
+
i
|
|
66
|
+
]]));
|
|
67
|
+
await match[1].append([
|
|
68
|
+
mk(0),
|
|
69
|
+
mk(1),
|
|
70
|
+
mk(2)
|
|
71
|
+
], undefined);
|
|
72
|
+
let sink = {
|
|
73
|
+
contents: []
|
|
74
|
+
};
|
|
75
|
+
let processed = await PgChangeFeedRelay_Runtime$ReventlessAws.relayWithPool(pool, "relay-basic", "relay-it", undefined, capturingSendBatch(sink));
|
|
76
|
+
globalThis.expect(processed).toBe(3);
|
|
77
|
+
globalThis.expect(sink.contents.length).toBe(3);
|
|
78
|
+
let first = sink.contents[0];
|
|
79
|
+
globalThis.expect(idOf(first)).toEqual("orderId:o-1");
|
|
80
|
+
let obj = Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(first), undefined);
|
|
81
|
+
globalThis.expect(JSON.stringify(Stdlib_Option.getOrThrow(obj["event"], undefined)).includes("OrderPlaced")).toBe(true);
|
|
82
|
+
let metaObj = Stdlib_Option.getOrThrow(Stdlib_Option.flatMap(obj["meta"], Stdlib_JSON.Decode.object), undefined);
|
|
83
|
+
globalThis.expect(metaObj["service"]).toEqual("relay-it");
|
|
84
|
+
});
|
|
85
|
+
globalThis.test("checkpoints — a second drain from the same subscriber sees nothing new", async () => {
|
|
86
|
+
let match = makeLog("relay-checkpoint");
|
|
87
|
+
let mk = i => stored("Fed", [{
|
|
88
|
+
key: "orderId",
|
|
89
|
+
value: "f"
|
|
90
|
+
}], Object.fromEntries([[
|
|
91
|
+
"i",
|
|
92
|
+
i
|
|
93
|
+
]]));
|
|
94
|
+
await match[1].append([
|
|
95
|
+
mk(0),
|
|
96
|
+
mk(1)
|
|
97
|
+
], undefined);
|
|
98
|
+
let sink = {
|
|
99
|
+
contents: []
|
|
100
|
+
};
|
|
101
|
+
let first = await PgChangeFeedRelay_Runtime$ReventlessAws.relayWithPool(pool, "relay-checkpoint", "relay-it", undefined, capturingSendBatch(sink));
|
|
102
|
+
globalThis.expect(first).toBe(2);
|
|
103
|
+
let second = await PgChangeFeedRelay_Runtime$ReventlessAws.relayWithPool(pool, "relay-checkpoint", "relay-it", undefined, capturingSendBatch(sink));
|
|
104
|
+
globalThis.expect(second).toBe(0);
|
|
105
|
+
globalThis.expect(sink.contents.length).toBe(2);
|
|
106
|
+
});
|
|
107
|
+
globalThis.test("partitionTag pins the id to its tag on a multi-tag event", async () => {
|
|
108
|
+
let match = DcbEventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, "relay-pt", [], {
|
|
109
|
+
TAG: "Simple",
|
|
110
|
+
_0: {
|
|
111
|
+
key: "customerId"
|
|
112
|
+
}
|
|
113
|
+
}, undefined, opts, undefined, undefined);
|
|
114
|
+
let ev = stored("OrderPlaced", [
|
|
115
|
+
{
|
|
116
|
+
key: "orderId",
|
|
117
|
+
value: "o-9"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
key: "customerId",
|
|
121
|
+
value: "c-9"
|
|
122
|
+
}
|
|
123
|
+
], Object.fromEntries([[
|
|
124
|
+
"n",
|
|
125
|
+
1
|
|
126
|
+
]]));
|
|
127
|
+
await match[1].append([ev], undefined);
|
|
128
|
+
let partitionTagJson = S.reverseConvertToJsonOrThrow({
|
|
129
|
+
TAG: "Simple",
|
|
130
|
+
_0: {
|
|
131
|
+
key: "customerId"
|
|
132
|
+
}
|
|
133
|
+
}, DcbTag$Reventless.derivedPartitionTagSchema);
|
|
134
|
+
let sink = {
|
|
135
|
+
contents: []
|
|
136
|
+
};
|
|
137
|
+
await PgChangeFeedRelay_Runtime$ReventlessAws.relayWithPool(pool, "relay-pt", "relay-it", partitionTagJson, capturingSendBatch(sink));
|
|
138
|
+
globalThis.expect(idOf(sink.contents[0])).toEqual("customerId:c-9");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
globalThis.test("Postgres relay integration (skipped — set PG_URL to run)", () => {
|
|
143
|
+
globalThis.expect(true).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export {
|
|
148
|
+
opts,
|
|
149
|
+
jsonObj,
|
|
150
|
+
stored,
|
|
151
|
+
capturingSendBatch,
|
|
152
|
+
idOf,
|
|
153
|
+
}
|
|
154
|
+
/* url Not a pure module */
|
|
@@ -42,3 +42,34 @@ describe("PgChangeFeedRelay_Runtime.toEventCollectorJson", () => {
|
|
|
42
42
|
expect(obj->Dict.get("id"))->toEqual(Some(JSON.Encode.string("dcb")))
|
|
43
43
|
})
|
|
44
44
|
})
|
|
45
|
+
|
|
46
|
+
describe("PgChangeFeedRelay_Runtime partitionTag (B2.3d)", () => {
|
|
47
|
+
let multiTagEvent = {
|
|
48
|
+
...sampleEvent,
|
|
49
|
+
tags: [{key: "orderId", value: "o-1"}, {key: "customerId", value: "c-9"}],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
testSync("a Simple partitionTag selects its tag for the id, not just the first", () => {
|
|
53
|
+
let pt = Reventless.DcbTag.Simple({key: "customerId"})
|
|
54
|
+
let json =
|
|
55
|
+
PgChangeFeedRelay_Runtime.toEventCollectorJson(multiTagEvent, ~partitionTag=pt)->Option.getOrThrow
|
|
56
|
+
let obj = json->JSON.Decode.object->Option.getOrThrow
|
|
57
|
+
// Without partitionTag the id would be "orderId:o-1" (first tag); the tag
|
|
58
|
+
// pins it to "customerId:c-9".
|
|
59
|
+
expect(obj->Dict.get("id"))->toEqual(Some(JSON.Encode.string("customerId:c-9")))
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
testSync("derivedPartitionTag round-trips through the sury schema (relay wire-format)", () => {
|
|
63
|
+
// The relay builder serialises partitionTag with this schema into HANDLER_CONFIG
|
|
64
|
+
// and PgChangeFeedRelay_Runtime.relay parses it back the same way. Guard the
|
|
65
|
+
// round-trip so a schema change can't silently break the id derivation.
|
|
66
|
+
let simple = Reventless.DcbTag.Simple({key: "courseId"})
|
|
67
|
+
let composite =
|
|
68
|
+
Reventless.DcbTag.Composite({keys: ["studentId", "courseId"], seps: [":"]})
|
|
69
|
+
[simple, composite]->Array.forEach(pt => {
|
|
70
|
+
let wire = pt->S.reverseConvertToJsonOrThrow(Reventless.DcbTag.derivedPartitionTagSchema)
|
|
71
|
+
let back = wire->S.parseJsonOrThrow(Reventless.DcbTag.derivedPartitionTagSchema)
|
|
72
|
+
expect(back)->toEqual(pt)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
3
4
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
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";
|
|
5
7
|
import * as PgChangeFeedRelay_Runtime$ReventlessAws from "../src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs";
|
|
6
8
|
|
|
7
9
|
let sampleEvent_data = Object.fromEntries([[
|
|
@@ -64,6 +66,69 @@ globalThis.describe("PgChangeFeedRelay_Runtime.toEventCollectorJson", () => {
|
|
|
64
66
|
});
|
|
65
67
|
});
|
|
66
68
|
|
|
69
|
+
globalThis.describe("PgChangeFeedRelay_Runtime partitionTag (B2.3d)", () => {
|
|
70
|
+
let multiTagEvent_data = sampleEvent_data;
|
|
71
|
+
let multiTagEvent_tags = [
|
|
72
|
+
{
|
|
73
|
+
key: "orderId",
|
|
74
|
+
value: "o-1"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: "customerId",
|
|
78
|
+
value: "c-9"
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
let multiTagEvent_meta = {
|
|
82
|
+
service: "Orders",
|
|
83
|
+
time: "2026-07-05T10:00:00.000Z",
|
|
84
|
+
msgId: "m-1",
|
|
85
|
+
correlationId: "m-1"
|
|
86
|
+
};
|
|
87
|
+
let multiTagEvent = {
|
|
88
|
+
position: "00000000000000000042:00000000000000000007",
|
|
89
|
+
eventType: "OrderCreated",
|
|
90
|
+
data: multiTagEvent_data,
|
|
91
|
+
tags: multiTagEvent_tags,
|
|
92
|
+
meta: multiTagEvent_meta,
|
|
93
|
+
recordedAt: "2026-07-05T10:00:00.000Z"
|
|
94
|
+
};
|
|
95
|
+
globalThis.test("a Simple partitionTag selects its tag for the id, not just the first", () => {
|
|
96
|
+
let json = Stdlib_Option.getOrThrow(PgChangeFeedRelay_Runtime$ReventlessAws.toEventCollectorJson(multiTagEvent, {
|
|
97
|
+
TAG: "Simple",
|
|
98
|
+
_0: {
|
|
99
|
+
key: "customerId"
|
|
100
|
+
}
|
|
101
|
+
}), undefined);
|
|
102
|
+
let obj = Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(json), undefined);
|
|
103
|
+
globalThis.expect(obj["id"]).toEqual("customerId:c-9");
|
|
104
|
+
});
|
|
105
|
+
globalThis.test("derivedPartitionTag round-trips through the sury schema (relay wire-format)", () => {
|
|
106
|
+
let composite = {
|
|
107
|
+
TAG: "Composite",
|
|
108
|
+
_0: {
|
|
109
|
+
keys: [
|
|
110
|
+
"studentId",
|
|
111
|
+
"courseId"
|
|
112
|
+
],
|
|
113
|
+
seps: [":"]
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
[
|
|
117
|
+
{
|
|
118
|
+
TAG: "Simple",
|
|
119
|
+
_0: {
|
|
120
|
+
key: "courseId"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
composite
|
|
124
|
+
].forEach(pt => {
|
|
125
|
+
let wire = S.reverseConvertToJsonOrThrow(pt, DcbTag$Reventless.derivedPartitionTagSchema);
|
|
126
|
+
let back = S.parseJsonOrThrow(wire, DcbTag$Reventless.derivedPartitionTagSchema);
|
|
127
|
+
globalThis.expect(back).toEqual(pt);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
67
132
|
export {
|
|
68
133
|
sampleEvent,
|
|
69
134
|
}
|