@reventlessdev/reventless-aws 3.0.0-alpha.232 → 3.0.0-alpha.233

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 (104) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +8 -8
  3. package/src/adapter/Api/AppSyncEventsSigner_Ops.res +137 -0
  4. package/src/adapter/Api/AppSyncEventsSigner_Ops.res.mjs +121 -0
  5. package/src/adapter/Api/Platform_AdminScan_Ops.res +136 -0
  6. package/src/adapter/Api/Platform_AdminScan_Ops.res.mjs +98 -0
  7. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +12 -90
  8. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +11 -84
  9. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +86 -0
  10. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +79 -0
  11. package/src/adapter/Api/Platform_UIFragments_Lambda.res +9 -79
  12. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +7 -143
  13. package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res +48 -0
  14. package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res.mjs +63 -0
  15. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +21 -80
  16. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +23 -68
  17. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res +70 -0
  18. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res.mjs +68 -0
  19. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +1 -1
  20. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +1 -1
  21. package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
  22. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -1
  23. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +42 -126
  24. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +246 -0
  25. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +145 -0
  26. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +27 -2
  27. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +10 -3
  28. package/src/adapter/Runtime/CounterEntryPoint.mjs +19 -73
  29. package/src/adapter/Runtime/CounterEntryPoint_Ops.res +145 -0
  30. package/src/adapter/Runtime/CounterEntryPoint_Ops.res.mjs +129 -0
  31. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +11 -1
  32. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +9 -3
  33. package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +52 -510
  34. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +816 -0
  35. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +651 -0
  36. package/src/adapter/Runtime/EventMapperEntryPoint.mjs +23 -67
  37. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res +126 -0
  38. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res.mjs +83 -0
  39. package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +16 -63
  40. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res +150 -0
  41. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res.mjs +111 -0
  42. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res +96 -0
  43. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res.mjs +70 -0
  44. package/src/adapter/Runtime/PgMigrationEntryPoint.res +54 -0
  45. package/src/adapter/Runtime/PgMigrationEntryPoint.res.mjs +37 -0
  46. package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +20 -55
  47. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +155 -0
  48. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +94 -0
  49. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +80 -0
  50. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res.mjs +39 -0
  51. package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res +4 -4
  52. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +34 -153
  53. package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res +114 -0
  54. package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res.mjs +83 -0
  55. package/src/adapter/Runtime/SideEffectEntryPoint.mjs +23 -64
  56. package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res +71 -0
  57. package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res.mjs +57 -0
  58. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +34 -165
  59. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +206 -0
  60. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res.mjs +105 -0
  61. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res +156 -0
  62. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res.mjs +123 -0
  63. package/src/adapter/Runtime/TaskBucketEntryPoint.mjs +18 -138
  64. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res +198 -0
  65. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res.mjs +155 -0
  66. package/src/adapter/StateTopic/StateTopic_AppSync.res +12 -170
  67. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +7 -167
  68. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res +208 -0
  69. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs +183 -0
  70. package/src/components/AutomationSlice_Builder.res +2 -0
  71. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  72. package/src/components/OutboundTranslationSlice_Builder.res +1 -0
  73. package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
  74. package/tests/AppSyncEventsSigner_OpsTest.res +75 -0
  75. package/tests/AppSyncEventsSigner_OpsTest.res.mjs +44 -0
  76. package/tests/AppSyncEventsSigner_ParityFixture.mjs +36 -0
  77. package/tests/AutomationSliceEntryPoint_OpsTest.res +165 -0
  78. package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +280 -0
  79. package/tests/CounterEntryPoint_OpsTest.res +115 -0
  80. package/tests/CounterEntryPoint_OpsTest.res.mjs +150 -0
  81. package/tests/EventCollectorEntryPoint_OpsTest.res +216 -0
  82. package/tests/EventCollectorEntryPoint_OpsTest.res.mjs +339 -0
  83. package/tests/EventMapperEntryPoint_OpsTest.res +103 -0
  84. package/tests/EventMapperEntryPoint_OpsTest.res.mjs +120 -0
  85. package/tests/ExtensionPointEntryPoint_OpsTest.res +57 -0
  86. package/tests/ExtensionPointEntryPoint_OpsTest.res.mjs +54 -0
  87. package/tests/PgMigrationEntryPoint_IntegrationTest.res +22 -31
  88. package/tests/PgMigrationEntryPoint_IntegrationTest.res.mjs +20 -23
  89. package/tests/PgQueryResolverEntryPoint_OpsTest.res +38 -0
  90. package/tests/PgQueryResolverEntryPoint_OpsTest.res.mjs +31 -0
  91. package/tests/Platform_AdminScan_OpsTest.res +80 -0
  92. package/tests/Platform_AdminScan_OpsTest.res.mjs +127 -0
  93. package/tests/ReadModelEntryPoint_OpsTest.res +141 -0
  94. package/tests/ReadModelEntryPoint_OpsTest.res.mjs +267 -0
  95. package/tests/SideEffectEntryPoint_OpsTest.res +79 -0
  96. package/tests/SideEffectEntryPoint_OpsTest.res.mjs +105 -0
  97. package/tests/StateViewSliceEntryPoint_OpsTest.res +167 -0
  98. package/tests/StateViewSliceEntryPoint_OpsTest.res.mjs +296 -0
  99. package/tests/TaskBucketEntryPoint_OpsTest.res +117 -0
  100. package/tests/TaskBucketEntryPoint_OpsTest.res.mjs +165 -0
  101. package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res +42 -13
  102. package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res.mjs +29 -6
  103. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +0 -57
  104. package/src/adapter/Runtime/PgMigrationEntryPoint.mjs +0 -40
@@ -0,0 +1,115 @@
1
+ // Guards the typed cold-start core hoisted out of CounterEntryPoint.mjs:
2
+ // - parseHandlerConfig — reads `publishChannelId`, the field the deploy side
3
+ // (CounterHandler_DynamoDbStream.res) actually writes; the former shell
4
+ // read `publishQueueUrl` and published CountFinished events to an
5
+ // undefined queue URL.
6
+ // - splitRecords — the references/counts stream routing: NewImage rows carry
7
+ // increments (unparsable inc falls back to 1), NewAndOldImage on the
8
+ // references stream is a duplicate to ignore, counts take the new image on
9
+ // insert AND update, and foreign records are dropped.
10
+
11
+ open JestGlobals
12
+
13
+ let attrS = (s: string): AwsSdk.DynamoDb.Util.attributeValue => {string: s}
14
+ let attrN = (n: string): AwsSdk.DynamoDb.Util.attributeValue => {number: n}
15
+
16
+ let refsArn = "arn:stream-references"
17
+ let countsArn = "arn:stream-counts"
18
+
19
+ let mkRecord = (
20
+ ~arn: string,
21
+ ~source="aws:dynamodb",
22
+ ~keysId: string,
23
+ ~newImage: option<dict<AwsSdk.DynamoDb.Util.attributeValue>>=?,
24
+ ~oldImage: option<dict<AwsSdk.DynamoDb.Util.attributeValue>>=?,
25
+ ): PulumiAws.DynamoDb.Stream.record => {
26
+ awsRegion: "eu-west-1",
27
+ dynamodb: {
28
+ keys: {id: attrS(keysId)},
29
+ newImage: ?newImage,
30
+ oldImage: ?oldImage,
31
+ },
32
+ eventID: "e1",
33
+ eventName: INSERT,
34
+ eventSource: source,
35
+ eventSourceARN: arn,
36
+ eventVersion: "1",
37
+ userIdentity: "",
38
+ }
39
+
40
+ describe("CounterEntryPoint_Ops.parseHandlerConfig", () => {
41
+ testSync("reads publishChannelId (the field the deploy side writes)", () => {
42
+ let config = CounterEntryPoint_Ops.parseHandlerConfig(
43
+ `{"targetSpecModule":"@x/p/src/Aggregate/Product.res.mjs","countsTableName":"counts","publishChannelId":"https://sqs/q","referencesStreamArn":"${refsArn}","countsStreamArn":"${countsArn}"}`,
44
+ )
45
+ expect(config.publishChannelId)->toEqual(Some("https://sqs/q"))
46
+ expect(config.countsTableName)->toEqual(Some("counts"))
47
+ expect(config.referencesStreamArn)->toEqual(Some(refsArn))
48
+ })
49
+ })
50
+
51
+ describe("CounterEntryPoint_Ops.splitRecords", () => {
52
+ testSync("routes references and counts records by source ARN", () => {
53
+ let records = [
54
+ // references insert: {id, inc} view row → increment 2
55
+ mkRecord(
56
+ ~arn=refsArn,
57
+ ~keysId="c1#ref-a",
58
+ ~newImage=Dict.fromArray([("id", attrS("c1#ref-a")), ("inc", attrN("2"))]),
59
+ ),
60
+ // references insert without a parsable inc → fallback 1
61
+ mkRecord(
62
+ ~arn=refsArn,
63
+ ~keysId="c1#ref-b",
64
+ ~newImage=Dict.fromArray([("id", attrS("c1#ref-b"))]),
65
+ ),
66
+ // references update (NewAndOldImage) → duplicate, ignored
67
+ mkRecord(
68
+ ~arn=refsArn,
69
+ ~keysId="c1#ref-a",
70
+ ~newImage=Dict.fromArray([("id", attrS("c1#ref-a")), ("inc", attrN("1"))]),
71
+ ~oldImage=Dict.fromArray([("id", attrS("c1#ref-a")), ("inc", attrN("1"))]),
72
+ ),
73
+ // counts insert AND update both take the new image
74
+ mkRecord(
75
+ ~arn=countsArn,
76
+ ~keysId="c1",
77
+ ~newImage=Dict.fromArray([("id", attrS("c1")), ("count", attrN("0"))]),
78
+ ),
79
+ mkRecord(
80
+ ~arn=countsArn,
81
+ ~keysId="c2",
82
+ ~newImage=Dict.fromArray([("id", attrS("c2")), ("count", attrN("3"))]),
83
+ ~oldImage=Dict.fromArray([("id", attrS("c2")), ("count", attrN("4"))]),
84
+ ),
85
+ // foreign stream / foreign source → dropped
86
+ mkRecord(
87
+ ~arn="arn:other",
88
+ ~keysId="x",
89
+ ~newImage=Dict.fromArray([("id", attrS("x"))]),
90
+ ),
91
+ mkRecord(
92
+ ~arn=refsArn,
93
+ ~source="aws:sqs",
94
+ ~keysId="y",
95
+ ~newImage=Dict.fromArray([("id", attrS("y"))]),
96
+ ),
97
+ ]
98
+
99
+ let (references, counts) = CounterEntryPoint_Ops.splitRecords(
100
+ ~referencesStreamArn=refsArn,
101
+ ~countsStreamArn=countsArn,
102
+ records,
103
+ )
104
+
105
+ expect(references)->toEqual([("c1#ref-a", 2), ("c1#ref-b", 1)])
106
+ expect(counts->Array.length)->toBe(2)
107
+ expect(
108
+ counts
109
+ ->Array.getUnsafe(1)
110
+ ->JSON.Decode.object
111
+ ->Option.flatMap(o => o->Dict.get("count"))
112
+ ->Option.flatMap(JSON.Decode.float),
113
+ )->toEqual(Some(3.))
114
+ })
115
+ })
@@ -0,0 +1,150 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as CounterEntryPoint_Ops$ReventlessAws from "../src/adapter/Runtime/CounterEntryPoint_Ops.res.mjs";
6
+
7
+ function attrS(s) {
8
+ return {
9
+ S: s
10
+ };
11
+ }
12
+
13
+ function attrN(n) {
14
+ return {
15
+ N: n
16
+ };
17
+ }
18
+
19
+ let refsArn = "arn:stream-references";
20
+
21
+ let countsArn = "arn:stream-counts";
22
+
23
+ function mkRecord(arn, sourceOpt, keysId, newImage, oldImage) {
24
+ let source = sourceOpt !== undefined ? sourceOpt : "aws:dynamodb";
25
+ return {
26
+ awsRegion: "eu-west-1",
27
+ dynamodb: {
28
+ Keys: {
29
+ id: attrS(keysId)
30
+ },
31
+ NewImage: newImage,
32
+ OldImage: oldImage
33
+ },
34
+ eventID: "e1",
35
+ eventName: "INSERT",
36
+ eventSource: source,
37
+ eventSourceARN: arn,
38
+ eventVersion: "1",
39
+ userIdentity: ""
40
+ };
41
+ }
42
+
43
+ globalThis.describe("CounterEntryPoint_Ops.parseHandlerConfig", () => {
44
+ globalThis.test("reads publishChannelId (the field the deploy side writes)", () => {
45
+ let config = CounterEntryPoint_Ops$ReventlessAws.parseHandlerConfig(`{"targetSpecModule":"@x/p/src/Aggregate/Product.res.mjs","countsTableName":"counts","publishChannelId":"https://sqs/q","referencesStreamArn":"` + refsArn + `","countsStreamArn":"` + countsArn + `"}`);
46
+ globalThis.expect(config.publishChannelId).toEqual("https://sqs/q");
47
+ globalThis.expect(config.countsTableName).toEqual("counts");
48
+ globalThis.expect(config.referencesStreamArn).toEqual(refsArn);
49
+ });
50
+ });
51
+
52
+ globalThis.describe("CounterEntryPoint_Ops.splitRecords", () => {
53
+ globalThis.test("routes references and counts records by source ARN", () => {
54
+ let records = [
55
+ mkRecord(refsArn, undefined, "c1#ref-a", Object.fromEntries([
56
+ [
57
+ "id",
58
+ attrS("c1#ref-a")
59
+ ],
60
+ [
61
+ "inc",
62
+ attrN("2")
63
+ ]
64
+ ]), undefined),
65
+ mkRecord(refsArn, undefined, "c1#ref-b", Object.fromEntries([[
66
+ "id",
67
+ attrS("c1#ref-b")
68
+ ]]), undefined),
69
+ mkRecord(refsArn, undefined, "c1#ref-a", Object.fromEntries([
70
+ [
71
+ "id",
72
+ attrS("c1#ref-a")
73
+ ],
74
+ [
75
+ "inc",
76
+ attrN("1")
77
+ ]
78
+ ]), Object.fromEntries([
79
+ [
80
+ "id",
81
+ attrS("c1#ref-a")
82
+ ],
83
+ [
84
+ "inc",
85
+ attrN("1")
86
+ ]
87
+ ])),
88
+ mkRecord(countsArn, undefined, "c1", Object.fromEntries([
89
+ [
90
+ "id",
91
+ attrS("c1")
92
+ ],
93
+ [
94
+ "count",
95
+ attrN("0")
96
+ ]
97
+ ]), undefined),
98
+ mkRecord(countsArn, undefined, "c2", Object.fromEntries([
99
+ [
100
+ "id",
101
+ attrS("c2")
102
+ ],
103
+ [
104
+ "count",
105
+ attrN("3")
106
+ ]
107
+ ]), Object.fromEntries([
108
+ [
109
+ "id",
110
+ attrS("c2")
111
+ ],
112
+ [
113
+ "count",
114
+ attrN("4")
115
+ ]
116
+ ])),
117
+ mkRecord("arn:other", undefined, "x", Object.fromEntries([[
118
+ "id",
119
+ attrS("x")
120
+ ]]), undefined),
121
+ mkRecord(refsArn, "aws:sqs", "y", Object.fromEntries([[
122
+ "id",
123
+ attrS("y")
124
+ ]]), undefined)
125
+ ];
126
+ let match = CounterEntryPoint_Ops$ReventlessAws.splitRecords(refsArn, countsArn, records);
127
+ let counts = match[1];
128
+ globalThis.expect(match[0]).toEqual([
129
+ [
130
+ "c1#ref-a",
131
+ 2
132
+ ],
133
+ [
134
+ "c1#ref-b",
135
+ 1
136
+ ]
137
+ ]);
138
+ globalThis.expect(counts.length).toBe(2);
139
+ globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(counts[1]), o => o["count"]), Stdlib_JSON.Decode.float)).toEqual(3);
140
+ });
141
+ });
142
+
143
+ export {
144
+ attrS,
145
+ attrN,
146
+ refsArn,
147
+ countsArn,
148
+ mkRecord,
149
+ }
150
+ /* Not a pure module */
@@ -0,0 +1,216 @@
1
+ // Guards the typed cold-start core hoisted out of EventCollectorEntryPoint.mjs:
2
+ // - projectPluginRow — the Plugin-RM row projection the admin cross-plugin
3
+ // subscription manager reads; a drift in the decode (a dropped nested
4
+ // field, a mis-defaulted optional) would silently break cross-plugin SNS
5
+ // wiring, invisible until a multi-plugin deploy.
6
+ // - parseHandlerConfig — the HANDLER_CONFIG contract every EventCollector
7
+ // Lambda cold-starts on: named errors for malformed deploys, the former JS
8
+ // normalizer's defaults, and the connectExtension null/absent → None rule.
9
+
10
+ open JestGlobals
11
+
12
+ let str = JSON.Encode.string
13
+ let obj = pairs => JSON.Encode.object(Dict.fromArray(pairs))
14
+
15
+ let project = EventCollectorEntryPoint_Ops.projectPluginRow
16
+
17
+ describe("EventCollectorEntryPoint_Ops.projectPluginRow", () => {
18
+ testSync("rejects a non-object row", () => {
19
+ expect((project(JSON.Encode.string("nope")))->Option.isNone)->toBe(true)
20
+ })
21
+
22
+ testSync("rejects a row missing id or status", () => {
23
+ expect(project(obj([("status", str("Connected"))]))->Option.isNone)->toBe(true)
24
+ expect(project(obj([("id", str("Catalog@1"))]))->Option.isNone)->toBe(true)
25
+ })
26
+
27
+ testSync("projects the id/status/eventCollector subset with defaults", () => {
28
+ let p = project(obj([("id", str("Catalog@1")), ("status", str("Connected"))]))->Option.getOrThrow
29
+ expect(p.id)->toBe("Catalog@1")
30
+ expect(p.status)->toBe("Connected")
31
+ expect(p.eventCollector)->toBe("") // defaulted when absent
32
+ expect(p.extensions->Array.length)->toBe(0)
33
+ expect(p.extensionPoints->Array.length)->toBe(0)
34
+ expect(p.dcbEventLog->Option.isNone)->toBe(true)
35
+ })
36
+
37
+ testSync("decodes extensions, dropping entries without extensionPointName", () => {
38
+ let row = obj([
39
+ ("id", str("Ordering@2")),
40
+ ("status", str("Connected")),
41
+ ("eventCollector", str("ec-arn")),
42
+ (
43
+ "extensions",
44
+ JSON.Encode.array([
45
+ obj([("name", str("OrdersExt")), ("extensionPointName", str("Catalog.Products")), ("dcbSources", JSON.Encode.array([str("prod"), JSON.Encode.int(7)]))]),
46
+ obj([("name", str("Malformed"))]), // no extensionPointName -> dropped
47
+ ]),
48
+ ),
49
+ ])
50
+ let p = project(row)->Option.getOrThrow
51
+ expect(p.eventCollector)->toBe("ec-arn")
52
+ expect(p.extensions->Array.length)->toBe(1)
53
+ let e = p.extensions->Array.getUnsafe(0)
54
+ expect(e.name)->toBe("OrdersExt")
55
+ expect(e.extensionPointName)->toBe("Catalog.Products")
56
+ // Non-string dcbSources elements are filtered out.
57
+ expect(e.dcbSources)->toEqual(["prod"])
58
+ })
59
+
60
+ testSync("decodes extensionPoints and dcbEventLog; drops EPs without name/eventTopic", () => {
61
+ let row = obj([
62
+ ("id", str("Catalog@1")),
63
+ ("status", str("Connected")),
64
+ (
65
+ "extensionPoints",
66
+ JSON.Encode.array([
67
+ obj([("name", str("Catalog.Products")), ("eventTopic", str("t-arn"))]),
68
+ obj([("name", str("NoTopic"))]), // no eventTopic -> dropped
69
+ ]),
70
+ ),
71
+ ("dcbEventLog", obj([("name", str("catalog-dcb")), ("eventTopicArn", str("dcb-arn"))])),
72
+ ])
73
+ let p = project(row)->Option.getOrThrow
74
+ expect(p.extensionPoints->Array.length)->toBe(1)
75
+ let ep = p.extensionPoints->Array.getUnsafe(0)
76
+ expect(ep.name)->toBe("Catalog.Products")
77
+ expect(ep.commandTopic)->toBe("") // defaulted
78
+ expect(ep.eventTopic)->toBe("t-arn")
79
+ let dcb = p.dcbEventLog->Option.getOrThrow
80
+ expect(dcb.name)->toBe("catalog-dcb")
81
+ expect(dcb.eventTopicArn)->toBe("dcb-arn")
82
+ })
83
+
84
+ testSync("drops a dcbEventLog missing eventTopicArn", () => {
85
+ let row = obj([
86
+ ("id", str("Catalog@1")),
87
+ ("status", str("Connected")),
88
+ ("dcbEventLog", obj([("name", str("catalog-dcb"))])),
89
+ ])
90
+ expect((project(row)->Option.getOrThrow).dcbEventLog->Option.isNone)->toBe(true)
91
+ })
92
+ })
93
+
94
+ // ── parseHandlerConfig ────────────────────────────────────────────────────────
95
+
96
+ // A minimal valid HANDLER_CONFIG (every required field present).
97
+ let minimalConfig = (extras: array<(string, JSON.t)>): string =>
98
+ Array.concat(
99
+ [
100
+ ("queueUrl", str("q-url")),
101
+ ("pluginExtensionPointCmdTopicUrl", str("ep-url")),
102
+ ("eventTopicArn", str("topic-arn")),
103
+ ("pluginReadModelTableName", str("plugin-rm")),
104
+ ("appSyncApiId", str("NOT_AVAILABLE")),
105
+ ("schedulerRoleArn", str("role-arn")),
106
+ ("schedulerQueueArn", str("")),
107
+ ("schedulerQueueName", str("")),
108
+ ("extensionPoints", JSON.Encode.array([])),
109
+ ("extensions", JSON.Encode.array([])),
110
+ ("publishToAggregates", obj([])),
111
+ ],
112
+ extras,
113
+ )
114
+ ->Dict.fromArray
115
+ ->JSON.Encode.object
116
+ ->JSON.stringify
117
+
118
+ let throws = (f: unit => 'a): option<string> =>
119
+ try {
120
+ let _ = f()
121
+ None
122
+ } catch {
123
+ | exn => exn->JsExn.fromException->Option.flatMap(JsExn.message)
124
+ }
125
+
126
+ describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
127
+ testSync("throws the named errors for empty / malformed / incomplete config", () => {
128
+ let msgOf = raw => throws(() => EventCollectorEntryPoint_Ops.parseHandlerConfig(raw))
129
+ expect(msgOf(""))->toEqual(Some("HANDLER_CONFIG env var is empty"))
130
+ expect(
131
+ msgOf("{nope")->Option.mapOr(false, m => m->String.startsWith("HANDLER_CONFIG JSON parse error:")),
132
+ )->toBe(true)
133
+ expect(msgOf(`{"queueUrl":"q"}`))->toEqual(
134
+ Some("HANDLER_CONFIG missing required field: pluginExtensionPointCmdTopicUrl"),
135
+ )
136
+ })
137
+
138
+ testSync("decodes a minimal config with empty-collection defaults", () => {
139
+ let c = EventCollectorEntryPoint_Ops.parseHandlerConfig(minimalConfig([]))
140
+ expect(c.queueUrl)->toBe("q-url")
141
+ expect(c.connectExtension->Option.isNone)->toBe(true) // absent → None
142
+ expect(c.readModelQueueUrls->Dict.keysToArray->Array.length)->toBe(0)
143
+ expect(c.readModelNamesForSourceName->Dict.keysToArray->Array.length)->toBe(0)
144
+ })
145
+
146
+ testSync("connectExtension: JSON null (admin) maps to None; a record decodes", () => {
147
+ let asNull = EventCollectorEntryPoint_Ops.parseHandlerConfig(
148
+ minimalConfig([("connectExtension", JSON.Encode.null)]),
149
+ )
150
+ expect(asNull.connectExtension->Option.isNone)->toBe(true)
151
+
152
+ let asRecord = EventCollectorEntryPoint_Ops.parseHandlerConfig(
153
+ minimalConfig([
154
+ (
155
+ "connectExtension",
156
+ obj([
157
+ ("specModule", str("spec.mjs")),
158
+ ("mappingsModule", str("map.mjs")),
159
+ ("extensionPointName", str("Platform.Plugin")),
160
+ ]),
161
+ ),
162
+ ]),
163
+ )
164
+ let ce = asRecord.connectExtension->Option.getOrThrow
165
+ expect(ce.extensionPointName)->toBe("Platform.Plugin")
166
+ })
167
+
168
+ testSync("extension entries get the former JS normalizer's defaults", () => {
169
+ let c = EventCollectorEntryPoint_Ops.parseHandlerConfig(
170
+ minimalConfig([
171
+ (
172
+ "extensions",
173
+ JSON.Encode.array([
174
+ // no name, no module specifiers, no aggregate/readModel lists
175
+ obj([("extensionPointName", str("Ordering.Orders"))]),
176
+ ]),
177
+ ),
178
+ ]),
179
+ )
180
+ let ext = c.extensions->Array.getUnsafe(0)
181
+ expect(ext.name)->toBe("Ordering.Orders") // name falls back to the EP name
182
+ expect(ext.specModule)->toBe("")
183
+ expect(ext.aggregateNames)->toEqual([])
184
+ expect(ext.readModelNames)->toEqual([])
185
+ })
186
+
187
+ testSync("decodes extensionPoints entries and the env-var maps", () => {
188
+ let c = EventCollectorEntryPoint_Ops.parseHandlerConfig(
189
+ minimalConfig([
190
+ (
191
+ "extensionPoints",
192
+ JSON.Encode.array([
193
+ obj([
194
+ ("specModule", str("s.mjs")),
195
+ ("mappingsModule", str("m.mjs")),
196
+ ("eventTopicArn", str("arn:ep")),
197
+ ("aggregateNames", JSON.Encode.array([str("Plugin")])),
198
+ ]),
199
+ ]),
200
+ ),
201
+ ("publishToAggregates", obj([("Plugin", str("PTA_Plugin_QUEUE_URL"))])),
202
+ (
203
+ "readModelNamesForSourceName",
204
+ obj([("Ordering.Orders", JSON.Encode.array([str("ProductDemands")]))]),
205
+ ),
206
+ ]),
207
+ )
208
+ let ep = c.extensionPoints->Array.getUnsafe(0)
209
+ expect(ep.eventTopicArn)->toBe("arn:ep")
210
+ expect(ep.aggregateNames)->toEqual(["Plugin"])
211
+ expect(c.publishToAggregates->Dict.get("Plugin"))->toEqual(Some("PTA_Plugin_QUEUE_URL"))
212
+ expect(c.readModelNamesForSourceName->Dict.get("Ordering.Orders"))->toEqual(
213
+ Some(["ProductDemands"]),
214
+ )
215
+ })
216
+ })