@reventlessdev/reventless-local 3.0.0-alpha.249 → 3.0.0-alpha.251

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 (174) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +13 -13
  3. package/src/BakedManifest.res +5 -6
  4. package/src/EmitCapabilities.res +3 -1
  5. package/src/LocalPlatformRegistry.res +3 -1
  6. package/src/LocalPlatformStart.res +7 -6
  7. package/src/LocalSeedTarget.res +3 -1
  8. package/src/Platform.res +291 -215
  9. package/src/ShellConfig.res +10 -17
  10. package/src/UiHints.res +8 -14
  11. package/src/UiSlots.res +10 -17
  12. package/src/adapter/Api/LocalEvents_Server.res +5 -3
  13. package/src/adapter/Api/LocalGraphQL_Adapter.res +4 -4
  14. package/src/adapter/Auth/LocalAuth.res +12 -16
  15. package/src/adapter/Auth/LocalAuth.res.mjs +2 -1
  16. package/src/adapter/Auth/UserStore.res +13 -40
  17. package/src/adapter/Auth/UserStore.res.mjs +11 -67
  18. package/src/adapter/Backend.res +1 -2
  19. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
  20. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
  21. package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
  22. package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
  23. package/src/adapter/Counter/LocalCounterHandler.res +1 -1
  24. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
  25. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
  26. package/src/adapter/DomainGraphQL_Server.res +35 -34
  27. package/src/adapter/DomainMCP_Server.res +45 -41
  28. package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
  29. package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
  30. package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
  31. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
  32. package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
  33. package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
  34. package/src/adapter/LocalBus.res +12 -12
  35. package/src/adapter/LocalCapabilities.res +6 -0
  36. package/src/adapter/LocalCapabilities.res.mjs +5 -2
  37. package/src/adapter/LocalGeocodeResolvers.res +1 -4
  38. package/src/adapter/LocalSideEffectHandler.res +1 -1
  39. package/src/adapter/LocalStateChangeDescriptor.res +2 -1
  40. package/src/adapter/MCP_ServerInstance.res +51 -39
  41. package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
  42. package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
  43. package/src/adapter/PlatformGraphQL_Server.res +10 -6
  44. package/src/adapter/PlatformGraphQL_Server.res.mjs +2 -1
  45. package/src/adapter/ProjectionCheckpoint.res +8 -1
  46. package/src/adapter/ProjectionPending.res +1 -3
  47. package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
  48. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
  49. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
  50. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
  51. package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
  52. package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
  53. package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
  54. package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
  55. package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
  56. package/src/adapter/SqliteDriver.res +16 -4
  57. package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
  58. package/src/components/Aggregate_Builder.res +6 -1
  59. package/src/components/Counter_Builder.res +1 -5
  60. package/src/components/DcbEventLog_Builder.res +5 -3
  61. package/src/components/ExtensionPoint_Builder.res +13 -15
  62. package/src/components/InboundTranslationSlice_Builder.res +3 -2
  63. package/src/components/OutboundTranslationSlice_Builder.res +3 -2
  64. package/src/components/ReadModel_Builder.res +18 -20
  65. package/src/components/StateViewSlice_Builder.res +3 -2
  66. package/src/components/Task_Builder.res +12 -13
  67. package/src/reset/LocalSeedReset.res +2 -5
  68. package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
  69. package/src/test/Mocks/MockEventLogStorage.res +8 -3
  70. package/src/test/Mocks/MockQueryDbStorage.res +3 -2
  71. package/src/test/TestRunner.res +1 -1
  72. package/tests/BakedManifestFilesTest.res +7 -10
  73. package/tests/LocalEventTapTest.res +26 -21
  74. package/tests/LocalPlatformStartTest.res +33 -28
  75. package/tests/PluginEventDecodeTest.res +3 -3
  76. package/tests/ShellConfigTest.res +30 -21
  77. package/tests/SplitApiFixtures.res +34 -10
  78. package/tests/SplitApiTest.res +92 -62
  79. package/tests/adapter/BackendParityTest.res +180 -130
  80. package/tests/adapter/CollectNEventsTest.res +6 -4
  81. package/tests/adapter/CommandAuthorizationTest.res +47 -41
  82. package/tests/adapter/CommandTopicChannelTest.res +163 -139
  83. package/tests/adapter/CounterHandlerTest.res +95 -77
  84. package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
  85. package/tests/adapter/DcbEventLogStorageTest.res +264 -207
  86. package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
  87. package/tests/adapter/EventCollectorChannelTest.res +153 -124
  88. package/tests/adapter/EventHistoryResolverTest.res +46 -18
  89. package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
  90. package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
  91. package/tests/adapter/EventTapTest.res +37 -20
  92. package/tests/adapter/EventTopicPublisherTest.res +74 -50
  93. package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
  94. package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
  95. package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
  96. package/tests/adapter/HeartbeatRunnerTest.res +99 -81
  97. package/tests/adapter/InboundTranslationMutationTest.res +4 -1
  98. package/tests/adapter/LocalAuthHttpTest.res +79 -85
  99. package/tests/adapter/LocalAuthLoginTest.res +42 -50
  100. package/tests/adapter/LocalAuthTest.res +8 -16
  101. package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
  102. package/tests/adapter/LocalBusBoundedTest.res +187 -117
  103. package/tests/adapter/LocalBusPubSubTest.res +118 -76
  104. package/tests/adapter/LocalDispatchContextTest.res +1 -2
  105. package/tests/adapter/LocalEvents_ServerTest.res +242 -187
  106. package/tests/adapter/PgProjectionCatchupTest.res +44 -10
  107. package/tests/adapter/ProjectionCheckpointTest.res +261 -206
  108. package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
  109. package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
  110. package/tests/adapter/QueryDbListResolverTest.res +225 -208
  111. package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
  112. package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
  113. package/tests/adapter/QueryDbStorageTest.res +163 -117
  114. package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
  115. package/tests/adapter/QueryEngineTest.res +179 -126
  116. package/tests/adapter/ScheduledPublisherTest.res +171 -135
  117. package/tests/adapter/ServedBucketHttpTest.res +65 -59
  118. package/tests/adapter/SortKey_FilterTest.res +19 -3
  119. package/tests/adapter/SqliteDriverTest.res +13 -10
  120. package/tests/adapter/TaskBucketTest.res +94 -80
  121. package/tests/components/aggregate/AggregateTest.res +31 -18
  122. package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
  123. package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
  124. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
  125. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
  126. package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
  127. package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
  128. package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
  129. package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
  130. package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
  131. package/tests/components/commandtopic/CommandTopicTest.res +24 -15
  132. package/tests/components/counter/CounterFixtures.res +6 -8
  133. package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
  134. package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
  135. package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
  136. package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
  137. package/tests/components/dcb/DcbFixtures.res +4 -1
  138. package/tests/components/dcb/DcbTest.res +9 -6
  139. package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
  140. package/tests/components/eventcollector/EventCollectorTest.res +6 -3
  141. package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
  142. package/tests/components/eventlog/EventLogConflictTest.res +8 -4
  143. package/tests/components/eventlog/EventLogFixtures.res +3 -3
  144. package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
  145. package/tests/components/eventlog/EventLogStreamTest.res +119 -56
  146. package/tests/components/eventlog/EventLogTest.res +20 -4
  147. package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
  148. package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
  149. package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
  150. package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
  151. package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
  152. package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
  153. package/tests/components/heartbeat/HeartbeatTest.res +56 -47
  154. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
  155. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
  156. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
  157. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
  158. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
  159. package/tests/components/querydb/QueryDbFixtures.res +1 -5
  160. package/tests/components/querydb/QueryDbTest.res +2 -4
  161. package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
  162. package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
  163. package/tests/components/readmodel/ReadModelFixtures.res +8 -8
  164. package/tests/components/scheduler/SchedulerTest.res +76 -58
  165. package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
  166. package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
  167. package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
  168. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
  169. package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
  170. package/tests/components/task/TaskFixtures.res +5 -5
  171. package/tests/components/task/TaskTest.res +95 -80
  172. package/tests/plugin/PluginsProjection_GWT.res +3 -1
  173. package/tests/plugin/UiFragments_GWT.res +6 -1
  174. package/tests/reset/LocalSeedResetTest.res +1 -0
@@ -18,111 +18,120 @@ describe("InboundTranslationSlice Callback", () => {
18
18
  })
19
19
 
20
20
  describe("receive", () => {
21
- testPromise("valid input with completed status publishes command and returns Ok", async () => {
22
- let publishedCommands = ref([])
23
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
24
- publishedCommands := cmds
25
- }
26
-
27
- let inputJson =
28
- {
21
+ testPromise(
22
+ "valid input with completed status publishes command and returns Ok",
23
+ async () => {
24
+ let publishedCommands = ref([])
25
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
26
+ publishedCommands := cmds
27
+ }
28
+
29
+ let inputJson = {
29
30
  "paymentId": "pay-123",
30
31
  "orderId": "ord-1",
31
32
  "status": "completed",
32
33
  }->Obj.magic
33
34
 
34
- let result = await Callback.receive(mockPublish, inputJson)
35
-
36
- switch result {
37
- | Ok({requestId, targetIds, commandCount}) =>
38
- expect(targetIds)->toEqual(["ord-1"])
39
- expect(commandCount)->toBe(1)
40
- // requestId keys the audit row, so the caller can correlate the response.
41
- expect(Callback.auditLog->Dict.get(requestId)->Option.isSome)->toBe(true)
42
- | Error(_) => expect(true)->toBe(false)
43
- }
44
- expect(publishedCommands.contents->Array.length)->toBe(1)
45
- let cmd = publishedCommands.contents->Array.getUnsafe(0)
46
- expect(cmd.id)->toBe("ord-1")
47
-
48
- // Audit log should have one entry
49
- let auditEntries = Callback.auditLog->Dict.toArray
50
- expect(auditEntries->Array.length)->toBe(1)
51
- let (_, auditRow) = auditEntries->Array.getUnsafe(0)
52
- expect(auditRow.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Success)
53
- expect(auditRow.commandCount)->toBe(Some(1))
54
- })
55
-
56
- testPromise("translate returns Error — no command published, audit logged", async () => {
57
- let publishedCommands = ref([])
58
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
59
- publishedCommands := cmds
60
- }
61
-
62
- let inputJson =
63
- {
35
+ let result = await Callback.receive(mockPublish, inputJson)
36
+
37
+ switch result {
38
+ | Ok({requestId, targetIds, commandCount}) =>
39
+ expect(targetIds)->toEqual(["ord-1"])
40
+ expect(commandCount)->toBe(1)
41
+ // requestId keys the audit row, so the caller can correlate the response.
42
+ expect(Callback.auditLog->Dict.get(requestId)->Option.isSome)->toBe(true)
43
+ | Error(_) => expect(true)->toBe(false)
44
+ }
45
+ expect(publishedCommands.contents->Array.length)->toBe(1)
46
+ let cmd = publishedCommands.contents->Array.getUnsafe(0)
47
+ expect(cmd.id)->toBe("ord-1")
48
+
49
+ // Audit log should have one entry
50
+ let auditEntries = Callback.auditLog->Dict.toArray
51
+ expect(auditEntries->Array.length)->toBe(1)
52
+ let (_, auditRow) = auditEntries->Array.getUnsafe(0)
53
+ expect(auditRow.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Success)
54
+ expect(auditRow.commandCount)->toBe(Some(1))
55
+ },
56
+ )
57
+
58
+ testPromise(
59
+ "translate returns Error no command published, audit logged",
60
+ async () => {
61
+ let publishedCommands = ref([])
62
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
63
+ publishedCommands := cmds
64
+ }
65
+
66
+ let inputJson = {
64
67
  "paymentId": "pay-123",
65
68
  "orderId": "ord-1",
66
69
  "status": "pending",
67
70
  }->Obj.magic
68
71
 
69
- let result = await Callback.receive(mockPublish, inputJson)
70
-
71
- switch result {
72
- | Error({error}) => expect(error)->toBe("Unknown payment status: pending")
73
- | Ok(_) => expect(true)->toBe(false)
74
- }
75
- expect(publishedCommands.contents->Array.length)->toBe(0)
76
-
77
- let auditEntries = Callback.auditLog->Dict.toArray
78
- expect(auditEntries->Array.length)->toBe(1)
79
- let (_, auditRow) = auditEntries->Array.getUnsafe(0)
80
- expect(auditRow.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Failure)
81
- })
82
-
83
- testPromise("invalid JSON input returns Error and audit logged", async () => {
84
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
85
-
86
- // Missing required fields
87
- let inputJson = {"unexpected": "data"}->Obj.magic
88
-
89
- let result = await Callback.receive(mockPublish, inputJson)
90
-
91
- switch result {
92
- | Error(_) => expect(true)->toBe(true)
93
- | Ok(_) => expect(true)->toBe(false)
94
- }
95
-
96
- let auditEntries = Callback.auditLog->Dict.toArray
97
- expect(auditEntries->Array.length)->toBe(1)
98
- let (_, auditRow) = auditEntries->Array.getUnsafe(0)
99
- expect(auditRow.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Failure)
100
- })
101
-
102
- testPromise("publishJsons failure returns Error and audit logged", async () => {
103
- let failingPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => {
104
- JsError.throwWithMessage("publish failed")
105
- }
106
-
107
- let inputJson =
108
- {
72
+ let result = await Callback.receive(mockPublish, inputJson)
73
+
74
+ switch result {
75
+ | Error({error}) => expect(error)->toBe("Unknown payment status: pending")
76
+ | Ok(_) => expect(true)->toBe(false)
77
+ }
78
+ expect(publishedCommands.contents->Array.length)->toBe(0)
79
+
80
+ let auditEntries = Callback.auditLog->Dict.toArray
81
+ expect(auditEntries->Array.length)->toBe(1)
82
+ let (_, auditRow) = auditEntries->Array.getUnsafe(0)
83
+ expect(auditRow.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Failure)
84
+ },
85
+ )
86
+
87
+ testPromise(
88
+ "invalid JSON input returns Error and audit logged",
89
+ async () => {
90
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
91
+
92
+ // Missing required fields
93
+ let inputJson = {"unexpected": "data"}->Obj.magic
94
+
95
+ let result = await Callback.receive(mockPublish, inputJson)
96
+
97
+ switch result {
98
+ | Error(_) => expect(true)->toBe(true)
99
+ | Ok(_) => expect(true)->toBe(false)
100
+ }
101
+
102
+ let auditEntries = Callback.auditLog->Dict.toArray
103
+ expect(auditEntries->Array.length)->toBe(1)
104
+ let (_, auditRow) = auditEntries->Array.getUnsafe(0)
105
+ expect(auditRow.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Failure)
106
+ },
107
+ )
108
+
109
+ testPromise(
110
+ "publishJsons failure returns Error and audit logged",
111
+ async () => {
112
+ let failingPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => {
113
+ JsError.throwWithMessage("publish failed")
114
+ }
115
+
116
+ let inputJson = {
109
117
  "paymentId": "pay-123",
110
118
  "orderId": "ord-1",
111
119
  "status": "completed",
112
120
  }->Obj.magic
113
121
 
114
- let result = await Callback.receive(failingPublish, inputJson)
122
+ let result = await Callback.receive(failingPublish, inputJson)
115
123
 
116
- switch result {
117
- | Error({error}) => expect(error)->toBe("publish failed")
118
- | Ok(_) => expect(true)->toBe(false)
119
- }
124
+ switch result {
125
+ | Error({error}) => expect(error)->toBe("publish failed")
126
+ | Ok(_) => expect(true)->toBe(false)
127
+ }
120
128
 
121
- let auditEntries = Callback.auditLog->Dict.toArray
122
- expect(auditEntries->Array.length)->toBe(1)
123
- let (_, auditRow) = auditEntries->Array.getUnsafe(0)
124
- expect(auditRow.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Failure)
125
- })
129
+ let auditEntries = Callback.auditLog->Dict.toArray
130
+ expect(auditEntries->Array.length)->toBe(1)
131
+ let (_, auditRow) = auditEntries->Array.getUnsafe(0)
132
+ expect(auditRow.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Failure)
133
+ },
134
+ )
126
135
  })
127
136
 
128
137
  // `auditLog` hands a row to whoever persists it and must not keep it: the dict
@@ -131,175 +140,188 @@ describe("InboundTranslationSlice Callback", () => {
131
140
  // every request — and, since the write overwrites by row id, would resurrect
132
141
  // rows deleted from the table in between. Persisting is per-request and takes.
133
142
  describe("draining the audit log", () => {
134
- testPromise("taking a row leaves the log empty for the next request", async () => {
135
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
136
- let input = status =>
137
- {"paymentId": "pay-1", "orderId": "ord-1", "status": status}->Obj.magic
138
-
139
- let first = await Callback.receive(mockPublish, input("completed"))
140
- let firstId = first->ReventlessCore.InboundTranslationSlice_Callback.requestIdOf
141
- expect(
142
- Callback.auditLog
143
- ->ReventlessCore.InboundTranslationSlice_Callback.takeAuditRow(firstId)
144
- ->Option.isSome,
145
- )->toBe(true)
146
- expect(Callback.auditLog->Dict.toArray->Array.length)->toBe(0)
147
-
148
- // A second request drains only its own row — the first one is gone, so it
149
- // cannot be written a second time.
150
- let second = await Callback.receive(mockPublish, input("completed"))
151
- let secondId = second->ReventlessCore.InboundTranslationSlice_Callback.requestIdOf
152
- expect(secondId == firstId)->toBe(false)
153
- expect(Callback.auditLog->Dict.toArray->Array.length)->toBe(1)
154
- expect(
155
- Callback.auditLog
156
- ->ReventlessCore.InboundTranslationSlice_Callback.takeAuditRow(firstId)
157
- ->Option.isSome,
158
- )->toBe(false)
159
- expect(
160
- Callback.auditLog
161
- ->ReventlessCore.InboundTranslationSlice_Callback.takeAuditRow(secondId)
162
- ->Option.isSome,
163
- )->toBe(true)
164
- })
165
-
166
- testPromise("a rejected request's row is drained the same way", async () => {
167
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
168
- let result = await Callback.receive(
169
- mockPublish,
170
- {"paymentId": "pay-1", "orderId": "ord-1", "status": "pending"}->Obj.magic,
171
- )
172
- let id = result->ReventlessCore.InboundTranslationSlice_Callback.requestIdOf
173
- switch Callback.auditLog->ReventlessCore.InboundTranslationSlice_Callback.takeAuditRow(id) {
174
- | Some(row) =>
175
- expect(row.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Failure)
176
- | None => expect(true)->toBe(false)
177
- }
178
- expect(Callback.auditLog->Dict.toArray->Array.length)->toBe(0)
179
- })
143
+ testPromise(
144
+ "taking a row leaves the log empty for the next request",
145
+ async () => {
146
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
147
+ let input = status =>
148
+ {"paymentId": "pay-1", "orderId": "ord-1", "status": status}->Obj.magic
149
+
150
+ let first = await Callback.receive(mockPublish, input("completed"))
151
+ let firstId = first->ReventlessCore.InboundTranslationSlice_Callback.requestIdOf
152
+ expect(
153
+ Callback.auditLog
154
+ ->ReventlessCore.InboundTranslationSlice_Callback.takeAuditRow(firstId)
155
+ ->Option.isSome,
156
+ )->toBe(true)
157
+ expect(Callback.auditLog->Dict.toArray->Array.length)->toBe(0)
158
+
159
+ // A second request drains only its own row — the first one is gone, so it
160
+ // cannot be written a second time.
161
+ let second = await Callback.receive(mockPublish, input("completed"))
162
+ let secondId = second->ReventlessCore.InboundTranslationSlice_Callback.requestIdOf
163
+ expect(secondId == firstId)->toBe(false)
164
+ expect(Callback.auditLog->Dict.toArray->Array.length)->toBe(1)
165
+ expect(
166
+ Callback.auditLog
167
+ ->ReventlessCore.InboundTranslationSlice_Callback.takeAuditRow(firstId)
168
+ ->Option.isSome,
169
+ )->toBe(false)
170
+ expect(
171
+ Callback.auditLog
172
+ ->ReventlessCore.InboundTranslationSlice_Callback.takeAuditRow(secondId)
173
+ ->Option.isSome,
174
+ )->toBe(true)
175
+ },
176
+ )
177
+
178
+ testPromise(
179
+ "a rejected request's row is drained the same way",
180
+ async () => {
181
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
182
+ let result = await Callback.receive(
183
+ mockPublish,
184
+ {"paymentId": "pay-1", "orderId": "ord-1", "status": "pending"}->Obj.magic,
185
+ )
186
+ let id = result->ReventlessCore.InboundTranslationSlice_Callback.requestIdOf
187
+ switch Callback.auditLog->ReventlessCore.InboundTranslationSlice_Callback.takeAuditRow(id) {
188
+ | Some(row) =>
189
+ expect(row.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Failure)
190
+ | None => expect(true)->toBe(false)
191
+ }
192
+ expect(Callback.auditLog->Dict.toArray->Array.length)->toBe(0)
193
+ },
194
+ )
180
195
  })
181
196
 
182
197
  describe("multi-command", () => {
183
- testPromise("translate returning multiple pairs publishes all commands", async () => {
184
- // Use a spec that returns multiple commands
185
- module MultiSpec = {
186
- let name = "BatchWebhook"
187
- let moduleUrl: string = %raw(`import.meta.url`)
188
-
189
- @schema
190
- type externalInput = {orderId: string, items: array<string>}
191
-
192
- @schema
193
- type command = ConfirmPayment({
194
- orderId: @s.matches(Reventless.DcbTag.string) string,
195
- paymentId: string,
196
- })
197
-
198
- let targetName = "ConfirmPayment"
199
- let externalSystem = None
200
-
201
- let translate = (input: externalInput) =>
202
- Ok(
203
- input.items->Array.map(item => (
204
- input.orderId,
205
- ConfirmPayment({orderId: input.orderId, paymentId: item}),
206
- )),
198
+ testPromise(
199
+ "translate returning multiple pairs publishes all commands",
200
+ async () => {
201
+ // Use a spec that returns multiple commands
202
+ module MultiSpec = {
203
+ let name = "BatchWebhook"
204
+ let moduleUrl: string = %raw(`import.meta.url`)
205
+
206
+ @schema
207
+ type externalInput = {orderId: string, items: array<string>}
208
+
209
+ @schema
210
+ type command =
211
+ | ConfirmPayment({
212
+ orderId: @s.matches(Reventless.DcbTag.string) string,
213
+ paymentId: string,
214
+ })
215
+
216
+ let targetName = "ConfirmPayment"
217
+ let externalSystem = None
218
+
219
+ let translate = (input: externalInput) => Ok(
220
+ input.items->Array.map(
221
+ item => (input.orderId, ConfirmPayment({orderId: input.orderId, paymentId: item})),
222
+ ),
207
223
  )
208
224
 
209
- let commandAuthorization = (_: command): Reventless.Authorization.permission =>
210
- AllowAuthenticated
211
- type lifecycleState = unit
212
- let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
213
- }
214
-
215
- module MultiTranslation = {
216
- let translate = MultiSpec.translate
217
- let moduleUrl = MultiSpec.moduleUrl
218
- }
219
- module MultiCallback = ReventlessCore.InboundTranslationSlice_Callback.Make(
220
- MultiSpec,
221
- MultiTranslation,
222
- )
223
-
224
- let publishedCommands = ref([])
225
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
226
- publishedCommands := cmds
227
- }
228
-
229
- let inputJson =
230
- {
225
+ let commandAuthorization = (_: command): Reventless.Authorization.permission =>
226
+ AllowAuthenticated
227
+ type lifecycleState = unit
228
+ let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> =>
229
+ Unrestricted
230
+ }
231
+
232
+ module MultiTranslation = {
233
+ let translate = MultiSpec.translate
234
+ let moduleUrl = MultiSpec.moduleUrl
235
+ }
236
+ module MultiCallback = ReventlessCore.InboundTranslationSlice_Callback.Make(
237
+ MultiSpec,
238
+ MultiTranslation,
239
+ )
240
+
241
+ let publishedCommands = ref([])
242
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
243
+ publishedCommands := cmds
244
+ }
245
+
246
+ let inputJson = {
231
247
  "orderId": "ord-1",
232
248
  "items": ["pay-1", "pay-2", "pay-3"],
233
249
  }->Obj.magic
234
250
 
235
- let result = await MultiCallback.receive(mockPublish, inputJson)
236
-
237
- switch result {
238
- | Ok({targetIds, commandCount}) =>
239
- expect(targetIds)->toEqual(["ord-1", "ord-1", "ord-1"])
240
- expect(commandCount)->toBe(3)
241
- | Error(_) => expect(true)->toBe(false)
242
- }
243
- // All 3 commands published in one batch
244
- expect(publishedCommands.contents->Array.length)->toBe(3)
245
-
246
- let auditEntries = MultiCallback.auditLog->Dict.toArray
247
- expect(auditEntries->Array.length)->toBe(1)
248
- let (_, auditRow) = auditEntries->Array.getUnsafe(0)
249
- expect(auditRow.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Success)
250
- expect(auditRow.commandCount)->toBe(Some(3))
251
- })
252
-
253
- testPromise("translate returning empty array publishes nothing", async () => {
254
- module EmptySpec = {
255
- let name = "EmptyWebhook"
256
- let moduleUrl: string = %raw(`import.meta.url`)
257
-
258
- @schema
259
- type externalInput = {orderId: string}
260
-
261
- @schema
262
- type command = ConfirmPayment({
263
- orderId: @s.matches(Reventless.DcbTag.string) string,
264
- paymentId: string,
265
- })
266
-
267
- let targetName = "ConfirmPayment"
268
- let externalSystem = None
269
-
270
- let translate = (_input: externalInput) => Ok([])
271
-
272
- let commandAuthorization = (_: command): Reventless.Authorization.permission =>
273
- AllowAuthenticated
274
- type lifecycleState = unit
275
- let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
276
- }
277
-
278
- module EmptyTranslation = {
279
- let translate = EmptySpec.translate
280
- let moduleUrl = EmptySpec.moduleUrl
281
- }
282
- module EmptyCallback = ReventlessCore.InboundTranslationSlice_Callback.Make(
283
- EmptySpec,
284
- EmptyTranslation,
285
- )
286
-
287
- let publishedCommands = ref([])
288
- let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
289
- publishedCommands := cmds
290
- }
291
-
292
- let inputJson = {"orderId": "ord-1"}->Obj.magic
293
-
294
- let result = await EmptyCallback.receive(mockPublish, inputJson)
295
-
296
- switch result {
297
- | Ok({targetIds, commandCount}) =>
298
- expect(targetIds)->toEqual([])
299
- expect(commandCount)->toBe(0)
300
- | Error(_) => expect(true)->toBe(false)
301
- }
302
- expect(publishedCommands.contents->Array.length)->toBe(0)
303
- })
251
+ let result = await MultiCallback.receive(mockPublish, inputJson)
252
+
253
+ switch result {
254
+ | Ok({targetIds, commandCount}) =>
255
+ expect(targetIds)->toEqual(["ord-1", "ord-1", "ord-1"])
256
+ expect(commandCount)->toBe(3)
257
+ | Error(_) => expect(true)->toBe(false)
258
+ }
259
+ // All 3 commands published in one batch
260
+ expect(publishedCommands.contents->Array.length)->toBe(3)
261
+
262
+ let auditEntries = MultiCallback.auditLog->Dict.toArray
263
+ expect(auditEntries->Array.length)->toBe(1)
264
+ let (_, auditRow) = auditEntries->Array.getUnsafe(0)
265
+ expect(auditRow.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Success)
266
+ expect(auditRow.commandCount)->toBe(Some(3))
267
+ },
268
+ )
269
+
270
+ testPromise(
271
+ "translate returning empty array publishes nothing",
272
+ async () => {
273
+ module EmptySpec = {
274
+ let name = "EmptyWebhook"
275
+ let moduleUrl: string = %raw(`import.meta.url`)
276
+
277
+ @schema
278
+ type externalInput = {orderId: string}
279
+
280
+ @schema
281
+ type command =
282
+ | ConfirmPayment({
283
+ orderId: @s.matches(Reventless.DcbTag.string) string,
284
+ paymentId: string,
285
+ })
286
+
287
+ let targetName = "ConfirmPayment"
288
+ let externalSystem = None
289
+
290
+ let translate = (_input: externalInput) => Ok([])
291
+
292
+ let commandAuthorization = (_: command): Reventless.Authorization.permission =>
293
+ AllowAuthenticated
294
+ type lifecycleState = unit
295
+ let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> =>
296
+ Unrestricted
297
+ }
298
+
299
+ module EmptyTranslation = {
300
+ let translate = EmptySpec.translate
301
+ let moduleUrl = EmptySpec.moduleUrl
302
+ }
303
+ module EmptyCallback = ReventlessCore.InboundTranslationSlice_Callback.Make(
304
+ EmptySpec,
305
+ EmptyTranslation,
306
+ )
307
+
308
+ let publishedCommands = ref([])
309
+ let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
310
+ publishedCommands := cmds
311
+ }
312
+
313
+ let inputJson = {"orderId": "ord-1"}->Obj.magic
314
+
315
+ let result = await EmptyCallback.receive(mockPublish, inputJson)
316
+
317
+ switch result {
318
+ | Ok({targetIds, commandCount}) =>
319
+ expect(targetIds)->toEqual([])
320
+ expect(commandCount)->toBe(0)
321
+ | Error(_) => expect(true)->toBe(false)
322
+ }
323
+ expect(publishedCommands.contents->Array.length)->toBe(0)
324
+ },
325
+ )
304
326
  })
305
327
  })
@@ -8,7 +8,7 @@ module OrderEventLog = {
8
8
  let moduleUrl: string = %raw(`import.meta.url`)
9
9
  @schema
10
10
  type event =
11
- | PaymentConfirmed({orderId: @s.matches(Reventless.DcbTag.string) string, paymentId: string})
11
+ PaymentConfirmed({orderId: @s.matches(Reventless.DcbTag.string) string, paymentId: string})
12
12
  }
13
13
 
14
14
  // ─────────────────────────────────────────────────────────────
@@ -23,7 +23,8 @@ module PaymentWebhookSpec = {
23
23
  type externalInput = {paymentId: string, orderId: string, status: string}
24
24
 
25
25
  @schema
26
- type command = ConfirmPayment({orderId: @s.matches(Reventless.DcbTag.string) string, paymentId: string})
26
+ type command =
27
+ ConfirmPayment({orderId: @s.matches(Reventless.DcbTag.string) string, paymentId: string})
27
28
 
28
29
  let targetName = "ConfirmPayment"
29
30
  let externalSystem = None