@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.
Files changed (28) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +6 -6
  3. package/src/Platform.res +77 -7
  4. package/src/Platform.res.mjs +77 -3
  5. package/src/adapter/DcbEventLog/DcbBackend.res +78 -0
  6. package/src/adapter/DcbEventLog/DcbBackend.res.mjs +53 -0
  7. package/src/adapter/DcbEventLog/DcbEventLogStorage.res +20 -0
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage.res.mjs +21 -1
  9. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +43 -0
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res.mjs +21 -0
  11. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +7 -0
  12. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +7 -1
  13. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +33 -8
  14. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +12 -3
  15. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +84 -6
  16. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +49 -4
  17. package/src/components/Api/AppSync_Adapter.res +3 -3
  18. package/src/components/Api/AppSync_Adapter.res.mjs +2 -2
  19. package/src/components/Plugin.res +1 -1
  20. package/src/components/Plugin.res.mjs +2 -2
  21. package/src/plugin/runtime/PluginRuntime_Builder.res +20 -0
  22. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +8 -0
  23. package/tests/AppSync_AdapterTest.res +12 -12
  24. package/tests/AppSync_AdapterTest.res.mjs +15 -15
  25. package/tests/PgChangeFeedRelay_IntegrationTest.res +148 -0
  26. package/tests/PgChangeFeedRelay_IntegrationTest.res.mjs +154 -0
  27. package/tests/PgChangeFeedRelay_RuntimeTest.res +31 -0
  28. 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 — iamCallable dual-auth", () => {
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("iamCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
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 entry_iamCallable = true;
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
- iamCallable: entry_iamCallable
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("iamCallable mutation without a group restriction stays open to Cognito + IAM", () => {
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 entry_iamCallable = true;
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
- iamCallable: entry_iamCallable
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-iamCallable sibling keeps single-mode @aws_auth (no @aws_iam)", () => {
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 entry_iamCallable = false;
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
- iamCallable: entry_iamCallable
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 iamEntry_iamCallable = true;
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
- iamCallable: iamEntry_iamCallable
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("iamCallable query applies dual-auth to BOTH single and list fields", () => {
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 entry_iamCallable = true;
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
- iamCallable: entry_iamCallable
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
  }