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

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 (170) hide show
  1. package/CHANGELOG.md +8 -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 +11 -15
  15. package/src/adapter/Auth/UserStore.res +5 -7
  16. package/src/adapter/Backend.res +1 -2
  17. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
  18. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
  19. package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
  20. package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
  21. package/src/adapter/Counter/LocalCounterHandler.res +1 -1
  22. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
  23. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
  24. package/src/adapter/DomainGraphQL_Server.res +35 -34
  25. package/src/adapter/DomainMCP_Server.res +45 -41
  26. package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
  27. package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
  28. package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
  29. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
  30. package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
  31. package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
  32. package/src/adapter/LocalBus.res +12 -12
  33. package/src/adapter/LocalCapabilities.res.mjs +1 -1
  34. package/src/adapter/LocalGeocodeResolvers.res +1 -4
  35. package/src/adapter/LocalSideEffectHandler.res +1 -1
  36. package/src/adapter/LocalStateChangeDescriptor.res +2 -1
  37. package/src/adapter/MCP_ServerInstance.res +51 -39
  38. package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
  39. package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
  40. package/src/adapter/PlatformGraphQL_Server.res +9 -5
  41. package/src/adapter/ProjectionCheckpoint.res +8 -1
  42. package/src/adapter/ProjectionPending.res +1 -3
  43. package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
  44. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
  45. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
  46. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
  47. package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
  48. package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
  49. package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
  50. package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
  51. package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
  52. package/src/adapter/SqliteDriver.res +16 -4
  53. package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
  54. package/src/components/Aggregate_Builder.res +6 -1
  55. package/src/components/Counter_Builder.res +1 -5
  56. package/src/components/DcbEventLog_Builder.res +5 -3
  57. package/src/components/ExtensionPoint_Builder.res +13 -15
  58. package/src/components/InboundTranslationSlice_Builder.res +3 -2
  59. package/src/components/OutboundTranslationSlice_Builder.res +3 -2
  60. package/src/components/ReadModel_Builder.res +18 -20
  61. package/src/components/StateViewSlice_Builder.res +3 -2
  62. package/src/components/Task_Builder.res +12 -13
  63. package/src/reset/LocalSeedReset.res +2 -5
  64. package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
  65. package/src/test/Mocks/MockEventLogStorage.res +8 -3
  66. package/src/test/Mocks/MockQueryDbStorage.res +3 -2
  67. package/src/test/TestRunner.res +1 -1
  68. package/tests/BakedManifestFilesTest.res +7 -10
  69. package/tests/LocalEventTapTest.res +26 -21
  70. package/tests/LocalPlatformStartTest.res +33 -28
  71. package/tests/PluginEventDecodeTest.res +3 -3
  72. package/tests/ShellConfigTest.res +30 -21
  73. package/tests/SplitApiFixtures.res +34 -10
  74. package/tests/SplitApiTest.res +92 -62
  75. package/tests/adapter/BackendParityTest.res +180 -130
  76. package/tests/adapter/CollectNEventsTest.res +6 -4
  77. package/tests/adapter/CommandAuthorizationTest.res +47 -41
  78. package/tests/adapter/CommandTopicChannelTest.res +163 -139
  79. package/tests/adapter/CounterHandlerTest.res +95 -77
  80. package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
  81. package/tests/adapter/DcbEventLogStorageTest.res +264 -207
  82. package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
  83. package/tests/adapter/EventCollectorChannelTest.res +153 -124
  84. package/tests/adapter/EventHistoryResolverTest.res +46 -18
  85. package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
  86. package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
  87. package/tests/adapter/EventTapTest.res +37 -20
  88. package/tests/adapter/EventTopicPublisherTest.res +74 -50
  89. package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
  90. package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
  91. package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
  92. package/tests/adapter/HeartbeatRunnerTest.res +99 -81
  93. package/tests/adapter/InboundTranslationMutationTest.res +4 -1
  94. package/tests/adapter/LocalAuthHttpTest.res +79 -85
  95. package/tests/adapter/LocalAuthLoginTest.res +42 -50
  96. package/tests/adapter/LocalAuthTest.res +8 -16
  97. package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
  98. package/tests/adapter/LocalBusBoundedTest.res +187 -117
  99. package/tests/adapter/LocalBusPubSubTest.res +118 -76
  100. package/tests/adapter/LocalDispatchContextTest.res +1 -2
  101. package/tests/adapter/LocalEvents_ServerTest.res +242 -187
  102. package/tests/adapter/PgProjectionCatchupTest.res +44 -10
  103. package/tests/adapter/ProjectionCheckpointTest.res +261 -206
  104. package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
  105. package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
  106. package/tests/adapter/QueryDbListResolverTest.res +225 -208
  107. package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
  108. package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
  109. package/tests/adapter/QueryDbStorageTest.res +163 -117
  110. package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
  111. package/tests/adapter/QueryEngineTest.res +179 -126
  112. package/tests/adapter/ScheduledPublisherTest.res +171 -135
  113. package/tests/adapter/ServedBucketHttpTest.res +65 -59
  114. package/tests/adapter/SortKey_FilterTest.res +19 -3
  115. package/tests/adapter/SqliteDriverTest.res +13 -10
  116. package/tests/adapter/TaskBucketTest.res +94 -80
  117. package/tests/components/aggregate/AggregateTest.res +31 -18
  118. package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
  119. package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
  120. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
  121. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
  122. package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
  123. package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
  124. package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
  125. package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
  126. package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
  127. package/tests/components/commandtopic/CommandTopicTest.res +24 -15
  128. package/tests/components/counter/CounterFixtures.res +6 -8
  129. package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
  130. package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
  131. package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
  132. package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
  133. package/tests/components/dcb/DcbFixtures.res +4 -1
  134. package/tests/components/dcb/DcbTest.res +9 -6
  135. package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
  136. package/tests/components/eventcollector/EventCollectorTest.res +6 -3
  137. package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
  138. package/tests/components/eventlog/EventLogConflictTest.res +8 -4
  139. package/tests/components/eventlog/EventLogFixtures.res +3 -3
  140. package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
  141. package/tests/components/eventlog/EventLogStreamTest.res +119 -56
  142. package/tests/components/eventlog/EventLogTest.res +20 -4
  143. package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
  144. package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
  145. package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
  146. package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
  147. package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
  148. package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
  149. package/tests/components/heartbeat/HeartbeatTest.res +56 -47
  150. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
  151. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
  152. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
  153. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
  154. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
  155. package/tests/components/querydb/QueryDbFixtures.res +1 -5
  156. package/tests/components/querydb/QueryDbTest.res +2 -4
  157. package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
  158. package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
  159. package/tests/components/readmodel/ReadModelFixtures.res +8 -8
  160. package/tests/components/scheduler/SchedulerTest.res +76 -58
  161. package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
  162. package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
  163. package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
  164. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
  165. package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
  166. package/tests/components/task/TaskFixtures.res +5 -5
  167. package/tests/components/task/TaskTest.res +95 -80
  168. package/tests/plugin/PluginsProjection_GWT.res +3 -1
  169. package/tests/plugin/UiFragments_GWT.res +6 -1
  170. package/tests/reset/LocalSeedResetTest.res +1 -0
@@ -15,7 +15,11 @@ let makeStorage = () =>
15
15
  ~opts,
16
16
  )
17
17
 
18
- let makeEvent = (~eventType, ~data, ~tags=[]): ReventlessCore.DcbEventLog_Adapter.rawStoredEvent => {
18
+ let makeEvent = (
19
+ ~eventType,
20
+ ~data,
21
+ ~tags=[],
22
+ ): ReventlessCore.DcbEventLog_Adapter.rawStoredEvent => {
19
23
  eventType,
20
24
  data,
21
25
  tags,
@@ -24,236 +28,289 @@ let makeEvent = (~eventType, ~data, ~tags=[]): ReventlessCore.DcbEventLog_Adapte
24
28
 
25
29
  describe("DcbEventLogStorage_InMemory", () => {
26
30
  describe("append", () => {
27
- testPromise("stores events and returns incrementing position", async () => {
28
- let storage = makeStorage()
29
- let ops = await storage.operations->TestRunner.resolve
30
- let result1 = await ops.append([makeEvent(~eventType="ItemCreated", ~data=JSON.Null)])
31
- let result2 = await ops.append([makeEvent(~eventType="ItemUpdated", ~data=JSON.Null)])
32
- expect(result1)->toEqual(Ok("1"))
33
- expect(result2)->toEqual(Ok("2"))
34
- })
31
+ testPromise(
32
+ "stores events and returns incrementing position",
33
+ async () => {
34
+ let storage = makeStorage()
35
+ let ops = await storage.operations->TestRunner.resolve
36
+ let result1 = await ops.append([makeEvent(~eventType="ItemCreated", ~data=JSON.Null)])
37
+ let result2 = await ops.append([makeEvent(~eventType="ItemUpdated", ~data=JSON.Null)])
38
+ expect(result1)->toEqual(Ok("1"))
39
+ expect(result2)->toEqual(Ok("2"))
40
+ },
41
+ )
35
42
 
36
- testPromise("appending multiple events at once returns position of last event", async () => {
37
- let storage = makeStorage()
38
- let ops = await storage.operations->TestRunner.resolve
39
- let result = await ops.append([
40
- makeEvent(~eventType="A", ~data=JSON.Null),
41
- makeEvent(~eventType="B", ~data=JSON.Null),
42
- ])
43
- expect(result)->toEqual(Ok("2"))
44
- })
43
+ testPromise(
44
+ "appending multiple events at once returns position of last event",
45
+ async () => {
46
+ let storage = makeStorage()
47
+ let ops = await storage.operations->TestRunner.resolve
48
+ let result = await ops.append([
49
+ makeEvent(~eventType="A", ~data=JSON.Null),
50
+ makeEvent(~eventType="B", ~data=JSON.Null),
51
+ ])
52
+ expect(result)->toEqual(Ok("2"))
53
+ },
54
+ )
45
55
  })
46
56
 
47
57
  describe("read", () => {
48
- testPromise("empty query returns all events", async () => {
49
- let storage = makeStorage()
50
- let ops = await storage.operations->TestRunner.resolve
51
- let _ = await ops.append([
52
- makeEvent(~eventType="Evt1", ~data=JSON.Null),
53
- makeEvent(~eventType="Evt2", ~data=JSON.Null),
54
- ])
55
- let result = await ops.read(~query=[])
56
- expect(result.events->Array.length)->toBe(2)
57
- })
58
+ testPromise(
59
+ "empty query returns all events",
60
+ async () => {
61
+ let storage = makeStorage()
62
+ let ops = await storage.operations->TestRunner.resolve
63
+ let _ = await ops.append([
64
+ makeEvent(~eventType="Evt1", ~data=JSON.Null),
65
+ makeEvent(~eventType="Evt2", ~data=JSON.Null),
66
+ ])
67
+ let result = await ops.read(~query=[])
68
+ expect(result.events->Array.length)->toBe(2)
69
+ },
70
+ )
58
71
 
59
- testPromise("filters by eventType", async () => {
60
- let storage = makeStorage()
61
- let ops = await storage.operations->TestRunner.resolve
62
- let _ = await ops.append([
63
- makeEvent(~eventType="Created", ~data=JSON.Null),
64
- makeEvent(~eventType="Updated", ~data=JSON.Null),
65
- makeEvent(~eventType="Created", ~data=JSON.Null),
66
- ])
67
- // Optional record fields use direct value syntax (not Some(...))
68
- let result = await ops.read(
69
- ~query=[{Reventless.DcbTag.eventTypes: ["Created"]}],
70
- )
71
- expect(result.events->Array.length)->toBe(2)
72
- let firstEvent = result.events->Array.getUnsafe(0)
73
- expect(firstEvent.eventType)->toBe("Created")
74
- })
72
+ testPromise(
73
+ "filters by eventType",
74
+ async () => {
75
+ let storage = makeStorage()
76
+ let ops = await storage.operations->TestRunner.resolve
77
+ let _ = await ops.append([
78
+ makeEvent(~eventType="Created", ~data=JSON.Null),
79
+ makeEvent(~eventType="Updated", ~data=JSON.Null),
80
+ makeEvent(~eventType="Created", ~data=JSON.Null),
81
+ ])
82
+ // Optional record fields use direct value syntax (not Some(...))
83
+ let result = await ops.read(~query=[{Reventless.DcbTag.eventTypes: ["Created"]}])
84
+ expect(result.events->Array.length)->toBe(2)
85
+ let firstEvent = result.events->Array.getUnsafe(0)
86
+ expect(firstEvent.eventType)->toBe("Created")
87
+ },
88
+ )
75
89
 
76
- testPromise("filters by tags", async () => {
77
- let storage = makeStorage()
78
- let ops = await storage.operations->TestRunner.resolve
79
- let _ = await ops.append([
80
- makeEvent(
81
- ~eventType="Evt",
82
- ~data=JSON.Null,
83
- ~tags=[{Reventless.DcbTag.key: "tenant", value: "acme"}],
84
- ),
85
- makeEvent(~eventType="Evt", ~data=JSON.Null, ~tags=[]),
86
- ])
87
- let result = await ops.read(
88
- ~query=[
89
- {
90
- Reventless.DcbTag.tags: [{Reventless.DcbTag.key: "tenant", value: "acme"}],
91
- },
92
- ],
93
- )
94
- expect(result.events->Array.length)->toBe(1)
95
- let firstEvent = result.events->Array.getUnsafe(0)
96
- expect(firstEvent.tags->Array.length)->toBe(1)
97
- })
90
+ testPromise(
91
+ "filters by tags",
92
+ async () => {
93
+ let storage = makeStorage()
94
+ let ops = await storage.operations->TestRunner.resolve
95
+ let _ = await ops.append([
96
+ makeEvent(
97
+ ~eventType="Evt",
98
+ ~data=JSON.Null,
99
+ ~tags=[{Reventless.DcbTag.key: "tenant", value: "acme"}],
100
+ ),
101
+ makeEvent(~eventType="Evt", ~data=JSON.Null, ~tags=[]),
102
+ ])
103
+ let result = await ops.read(
104
+ ~query=[
105
+ {
106
+ Reventless.DcbTag.tags: [{Reventless.DcbTag.key: "tenant", value: "acme"}],
107
+ },
108
+ ],
109
+ )
110
+ expect(result.events->Array.length)->toBe(1)
111
+ let firstEvent = result.events->Array.getUnsafe(0)
112
+ expect(firstEvent.tags->Array.length)->toBe(1)
113
+ },
114
+ )
98
115
 
99
- testPromise("multi-tag clause matches only events carrying ALL tags", async () => {
100
- let storage = makeStorage()
101
- let ops = await storage.operations->TestRunner.resolve
102
- let _ = await ops.append([
103
- makeEvent(
104
- ~eventType="Evt",
105
- ~data=JSON.Null,
106
- ~tags=[{Reventless.DcbTag.key: "tenant", value: "acme"}, {key: "region", value: "eu"}],
107
- ),
108
- makeEvent(~eventType="Evt", ~data=JSON.Null, ~tags=[{Reventless.DcbTag.key: "tenant", value: "acme"}]),
109
- makeEvent(~eventType="Evt", ~data=JSON.Null, ~tags=[{Reventless.DcbTag.key: "region", value: "eu"}]),
110
- ])
111
- // Intersection of the two tags' posting lists → only the first event.
112
- let result = await ops.read(
113
- ~query=[
114
- {
115
- Reventless.DcbTag.tags: [
116
- {Reventless.DcbTag.key: "tenant", value: "acme"},
117
- {key: "region", value: "eu"},
118
- ],
119
- },
120
- ],
121
- )
122
- expect(result.events->Array.length)->toBe(1)
123
- })
116
+ testPromise(
117
+ "multi-tag clause matches only events carrying ALL tags",
118
+ async () => {
119
+ let storage = makeStorage()
120
+ let ops = await storage.operations->TestRunner.resolve
121
+ let _ = await ops.append([
122
+ makeEvent(
123
+ ~eventType="Evt",
124
+ ~data=JSON.Null,
125
+ ~tags=[{Reventless.DcbTag.key: "tenant", value: "acme"}, {key: "region", value: "eu"}],
126
+ ),
127
+ makeEvent(
128
+ ~eventType="Evt",
129
+ ~data=JSON.Null,
130
+ ~tags=[{Reventless.DcbTag.key: "tenant", value: "acme"}],
131
+ ),
132
+ makeEvent(
133
+ ~eventType="Evt",
134
+ ~data=JSON.Null,
135
+ ~tags=[{Reventless.DcbTag.key: "region", value: "eu"}],
136
+ ),
137
+ ])
138
+ // Intersection of the two tags' posting lists → only the first event.
139
+ let result = await ops.read(
140
+ ~query=[
141
+ {
142
+ Reventless.DcbTag.tags: [
143
+ {Reventless.DcbTag.key: "tenant", value: "acme"},
144
+ {key: "region", value: "eu"},
145
+ ],
146
+ },
147
+ ],
148
+ )
149
+ expect(result.events->Array.length)->toBe(1)
150
+ },
151
+ )
124
152
 
125
- testPromise("OR of clauses unions matches across clauses in position order", async () => {
126
- let storage = makeStorage()
127
- let ops = await storage.operations->TestRunner.resolve
128
- let _ = await ops.append([
129
- makeEvent(~eventType="A", ~data=JSON.Null),
130
- makeEvent(~eventType="B", ~data=JSON.Null, ~tags=[{Reventless.DcbTag.key: "x", value: "1"}]),
131
- makeEvent(~eventType="C", ~data=JSON.Null),
132
- ])
133
- // Clause 1 (type A) ∪ clause 2 (tag x=1) → events at positions 1 and 2.
134
- let result = await ops.read(
135
- ~query=[
136
- {Reventless.DcbTag.eventTypes: ["A"]},
137
- {Reventless.DcbTag.tags: [{Reventless.DcbTag.key: "x", value: "1"}]},
138
- ],
139
- )
140
- expect(result.events->Array.length)->toBe(2)
141
- expect((result.events->Array.getUnsafe(0)).eventType)->toBe("A")
142
- expect((result.events->Array.getUnsafe(1)).eventType)->toBe("B")
143
- })
153
+ testPromise(
154
+ "OR of clauses unions matches across clauses in position order",
155
+ async () => {
156
+ let storage = makeStorage()
157
+ let ops = await storage.operations->TestRunner.resolve
158
+ let _ = await ops.append([
159
+ makeEvent(~eventType="A", ~data=JSON.Null),
160
+ makeEvent(
161
+ ~eventType="B",
162
+ ~data=JSON.Null,
163
+ ~tags=[{Reventless.DcbTag.key: "x", value: "1"}],
164
+ ),
165
+ makeEvent(~eventType="C", ~data=JSON.Null),
166
+ ])
167
+ // Clause 1 (type A) ∪ clause 2 (tag x=1) → events at positions 1 and 2.
168
+ let result = await ops.read(
169
+ ~query=[
170
+ {Reventless.DcbTag.eventTypes: ["A"]},
171
+ {Reventless.DcbTag.tags: [{Reventless.DcbTag.key: "x", value: "1"}]},
172
+ ],
173
+ )
174
+ expect(result.events->Array.length)->toBe(2)
175
+ expect((result.events->Array.getUnsafe(0)).eventType)->toBe("A")
176
+ expect((result.events->Array.getUnsafe(1)).eventType)->toBe("B")
177
+ },
178
+ )
144
179
 
145
180
  // An empty tag value is a legitimate model state — an absent member of a
146
181
  // composite partition key. Every backend must accept it, record it verbatim,
147
182
  // and keep matching the event through its partition and composite reads. See
148
183
  // docs/plans/done/dcb-empty-tag-values-break-append.md.
149
- testPromise("an empty tag value appends and stays readable", async () => {
150
- let storage = makeStorage()
151
- let ops = await storage.operations->TestRunner.resolve
152
- let tags = [
153
- {Reventless.DcbTag.key: "itemId", value: "i1"},
154
- {Reventless.DcbTag.key: "variantId", value: ""},
155
- ]
156
- let appended = await ops.append([makeEvent(~eventType="ItemAdded", ~data=JSON.Null, ~tags)])
157
- expect(appended)->toEqual(Ok("1"))
184
+ testPromise(
185
+ "an empty tag value appends and stays readable",
186
+ async () => {
187
+ let storage = makeStorage()
188
+ let ops = await storage.operations->TestRunner.resolve
189
+ let tags = [
190
+ {Reventless.DcbTag.key: "itemId", value: "i1"},
191
+ {Reventless.DcbTag.key: "variantId", value: ""},
192
+ ]
193
+ let appended = await ops.append([makeEvent(~eventType="ItemAdded", ~data=JSON.Null, ~tags)])
194
+ expect(appended)->toEqual(Ok("1"))
158
195
 
159
- let byPartition = await ops.read(
160
- ~query=[{Reventless.DcbTag.tags: [{Reventless.DcbTag.key: "itemId", value: "i1"}]}],
161
- )
162
- expect(byPartition.events->Array.length)->toBe(1)
163
- // The empty value is recorded verbatim, not dropped or substituted.
164
- expect((byPartition.events->Array.getUnsafe(0)).tags->Array.map(t => (t.key, t.value)))
165
- ->toEqual([("itemId", "i1"), ("variantId", "")])
196
+ let byPartition = await ops.read(
197
+ ~query=[{Reventless.DcbTag.tags: [{Reventless.DcbTag.key: "itemId", value: "i1"}]}],
198
+ )
199
+ expect(byPartition.events->Array.length)->toBe(1)
200
+ // The empty value is recorded verbatim, not dropped or substituted.
201
+ expect(
202
+ (byPartition.events->Array.getUnsafe(0)).tags->Array.map(t => (t.key, t.value)),
203
+ )->toEqual([("itemId", "i1"), ("variantId", "")])
166
204
 
167
- let byComposite = await ops.read(~query=[{Reventless.DcbTag.tags: tags}])
168
- expect(byComposite.events->Array.length)->toBe(1)
169
- })
205
+ let byComposite = await ops.read(~query=[{Reventless.DcbTag.tags: tags}])
206
+ expect(byComposite.events->Array.length)->toBe(1)
207
+ },
208
+ )
170
209
 
171
- testPromise("after parameter skips events at or before that position", async () => {
172
- let storage = makeStorage()
173
- let ops = await storage.operations->TestRunner.resolve
174
- let _ = await ops.append([
175
- makeEvent(~eventType="E1", ~data=JSON.Null),
176
- makeEvent(~eventType="E2", ~data=JSON.Null),
177
- makeEvent(~eventType="E3", ~data=JSON.Null),
178
- ])
179
- let result = await ops.read(~query=[], ~after="1")
180
- // Events at position > "1" — positions 2 and 3
181
- expect(result.events->Array.length)->toBe(2)
182
- let firstEvent = result.events->Array.getUnsafe(0)
183
- expect(firstEvent.eventType)->toBe("E2")
184
- })
210
+ testPromise(
211
+ "after parameter skips events at or before that position",
212
+ async () => {
213
+ let storage = makeStorage()
214
+ let ops = await storage.operations->TestRunner.resolve
215
+ let _ = await ops.append([
216
+ makeEvent(~eventType="E1", ~data=JSON.Null),
217
+ makeEvent(~eventType="E2", ~data=JSON.Null),
218
+ makeEvent(~eventType="E3", ~data=JSON.Null),
219
+ ])
220
+ let result = await ops.read(~query=[], ~after="1")
221
+ // Events at position > "1" — positions 2 and 3
222
+ expect(result.events->Array.length)->toBe(2)
223
+ let firstEvent = result.events->Array.getUnsafe(0)
224
+ expect(firstEvent.eventType)->toBe("E2")
225
+ },
226
+ )
185
227
  })
186
228
 
187
229
  describe("headPosition", () => {
188
- testPromise("equals the position of the last appended event", async () => {
189
- let storage = makeStorage()
190
- let ops = await storage.operations->TestRunner.resolve
191
- let _ = await ops.append([
192
- makeEvent(~eventType="E1", ~data=JSON.Null),
193
- makeEvent(~eventType="E2", ~data=JSON.Null),
194
- ])
195
- let result = await ops.read(~query=[])
196
- expect(result.headPosition)->toEqual(Some("2"))
197
- })
230
+ testPromise(
231
+ "equals the position of the last appended event",
232
+ async () => {
233
+ let storage = makeStorage()
234
+ let ops = await storage.operations->TestRunner.resolve
235
+ let _ = await ops.append([
236
+ makeEvent(~eventType="E1", ~data=JSON.Null),
237
+ makeEvent(~eventType="E2", ~data=JSON.Null),
238
+ ])
239
+ let result = await ops.read(~query=[])
240
+ expect(result.headPosition)->toEqual(Some("2"))
241
+ },
242
+ )
198
243
 
199
- testPromise("headPosition is absent when no events have been stored", async () => {
200
- let storage = makeStorage()
201
- let ops = await storage.operations->TestRunner.resolve
202
- let result = await ops.read(~query=[])
203
- expect(result.headPosition)->toEqual(None)
204
- })
244
+ testPromise(
245
+ "headPosition is absent when no events have been stored",
246
+ async () => {
247
+ let storage = makeStorage()
248
+ let ops = await storage.operations->TestRunner.resolve
249
+ let result = await ops.read(~query=[])
250
+ expect(result.headPosition)->toEqual(None)
251
+ },
252
+ )
205
253
  })
206
254
 
207
255
  describe("conditional append", () => {
208
- testPromise("append with matching condition query returns Error (conflict)", async () => {
209
- let storage = makeStorage()
210
- let ops = await storage.operations->TestRunner.resolve
211
- let _ = await ops.append([makeEvent(~eventType="ItemCreated", ~data=JSON.Null)])
212
- let condition: Reventless.DcbTag.appendCondition = {
213
- query: [{Reventless.DcbTag.eventTypes: ["ItemCreated"]}],
214
- }
215
- let result = await ops.append(
216
- [makeEvent(~eventType="ItemUpdated", ~data=JSON.Null)],
217
- ~condition,
218
- )
219
- expect(
220
- switch result {
221
- | Error(_) => true
222
- | Ok(_) => false
223
- },
224
- )->toBe(true)
225
- })
256
+ testPromise(
257
+ "append with matching condition query returns Error (conflict)",
258
+ async () => {
259
+ let storage = makeStorage()
260
+ let ops = await storage.operations->TestRunner.resolve
261
+ let _ = await ops.append([makeEvent(~eventType="ItemCreated", ~data=JSON.Null)])
262
+ let condition: Reventless.DcbTag.appendCondition = {
263
+ query: [{Reventless.DcbTag.eventTypes: ["ItemCreated"]}],
264
+ }
265
+ let result = await ops.append(
266
+ [makeEvent(~eventType="ItemUpdated", ~data=JSON.Null)],
267
+ ~condition,
268
+ )
269
+ expect(
270
+ switch result {
271
+ | Error(_) => true
272
+ | Ok(_) => false
273
+ },
274
+ )->toBe(true)
275
+ },
276
+ )
226
277
 
227
- testPromise("append with condition that matches no events succeeds", async () => {
228
- let storage = makeStorage()
229
- let ops = await storage.operations->TestRunner.resolve
230
- let _ = await ops.append([makeEvent(~eventType="OtherEvent", ~data=JSON.Null)])
231
- let condition: Reventless.DcbTag.appendCondition = {
232
- query: [{Reventless.DcbTag.eventTypes: ["ItemCreated"]}],
233
- }
234
- let result = await ops.append(
235
- [makeEvent(~eventType="ItemCreated", ~data=JSON.Null)],
236
- ~condition,
237
- )
238
- expect(result)->toEqual(Ok("2"))
239
- })
278
+ testPromise(
279
+ "append with condition that matches no events succeeds",
280
+ async () => {
281
+ let storage = makeStorage()
282
+ let ops = await storage.operations->TestRunner.resolve
283
+ let _ = await ops.append([makeEvent(~eventType="OtherEvent", ~data=JSON.Null)])
284
+ let condition: Reventless.DcbTag.appendCondition = {
285
+ query: [{Reventless.DcbTag.eventTypes: ["ItemCreated"]}],
286
+ }
287
+ let result = await ops.append(
288
+ [makeEvent(~eventType="ItemCreated", ~data=JSON.Null)],
289
+ ~condition,
290
+ )
291
+ expect(result)->toEqual(Ok("2"))
292
+ },
293
+ )
240
294
 
241
- testPromise("condition with after only checks events after that position", async () => {
242
- let storage = makeStorage()
243
- let ops = await storage.operations->TestRunner.resolve
244
- // Position 1: ItemCreated
245
- let _ = await ops.append([makeEvent(~eventType="ItemCreated", ~data=JSON.Null)])
246
- // Condition checks for ItemCreated only AFTER position "1" — no such events
247
- let condition: Reventless.DcbTag.appendCondition = {
248
- query: [{Reventless.DcbTag.eventTypes: ["ItemCreated"]}],
249
- after: "1",
250
- }
251
- let result = await ops.append(
252
- [makeEvent(~eventType="ItemUpdated", ~data=JSON.Null)],
253
- ~condition,
254
- )
255
- // No conflict because ItemCreated is at position 1, not > 1
256
- expect(result)->toEqual(Ok("2"))
257
- })
295
+ testPromise(
296
+ "condition with after only checks events after that position",
297
+ async () => {
298
+ let storage = makeStorage()
299
+ let ops = await storage.operations->TestRunner.resolve
300
+ // Position 1: ItemCreated
301
+ let _ = await ops.append([makeEvent(~eventType="ItemCreated", ~data=JSON.Null)])
302
+ // Condition checks for ItemCreated only AFTER position "1" — no such events
303
+ let condition: Reventless.DcbTag.appendCondition = {
304
+ query: [{Reventless.DcbTag.eventTypes: ["ItemCreated"]}],
305
+ after: "1",
306
+ }
307
+ let result = await ops.append(
308
+ [makeEvent(~eventType="ItemUpdated", ~data=JSON.Null)],
309
+ ~condition,
310
+ )
311
+ // No conflict because ItemCreated is at position 1, not > 1
312
+ expect(result)->toEqual(Ok("2"))
313
+ },
314
+ )
258
315
  })
259
316
  })
@@ -11,12 +11,12 @@ module Server = DomainGraphQL_Server
11
11
  // Execute a query directly against a composed schema (no HTTP server needed).
12
12
  type executionResult = {data: Nullable.t<JSON.t>, errors: Nullable.t<array<JSON.t>>}
13
13
  @module("graphql")
14
- external graphqlExecute: {"schema": GraphqlYoga.schema, "source": string} => promise<executionResult> =
15
- "graphql"
14
+ external graphqlExecute: {"schema": GraphqlYoga.schema, "source": string} => promise<
15
+ executionResult,
16
+ > = "graphql"
16
17
 
17
18
  let intResolver = (value: int): Server.resolverFn =>
18
- async (_root, _args, _ctx) =>
19
- JSON.Encode.object(Dict.fromArray([("x", JSON.Encode.int(value))]))
19
+ async (_root, _args, _ctx) => JSON.Encode.object(Dict.fromArray([("x", JSON.Encode.int(value))]))
20
20
 
21
21
  let resolversOf = (entries: array<(string, Server.resolverFn)>) => Dict.fromArray(entries)
22
22
 
@@ -67,27 +67,30 @@ test("two scopes with identical shared-type copies compose and both fields resol
67
67
 
68
68
  // ── (b) standalone-invalid scope fails with plugin-name attribution ─────────
69
69
 
70
- testSync("scope referencing an undefined type fails standalone validation naming the plugin", () => {
71
- // Register under a construction token, then relabel to the plugin name —
72
- // exercising the Platform.res flow (name only known after construction).
73
- Server.setScope("plugin-token-1")
74
- Server.registerQueries(
75
- ~sdlFields=[" broken: MissingType"],
76
- ~resolvers=resolversOf([("broken", intResolver(0))]),
77
- )
78
- Server.relabelScope(~from="plugin-token-1", ~to_="BrokenPlugin")
79
- Server.resetScope()
80
-
81
- switch Server.composeSchema() {
82
- | _ => Runner.fail("expected composeSchema to throw")
83
- | exception JsExn(err) => {
84
- expect(exnMessage(err))->toContain(
85
- `Plugin "BrokenPlugin" subgraph document is not valid standalone`,
86
- )
87
- expect(exnMessage(err))->toContain("MissingType")
70
+ testSync(
71
+ "scope referencing an undefined type fails standalone validation naming the plugin",
72
+ () => {
73
+ // Register under a construction token, then relabel to the plugin name —
74
+ // exercising the Platform.res flow (name only known after construction).
75
+ Server.setScope("plugin-token-1")
76
+ Server.registerQueries(
77
+ ~sdlFields=[" broken: MissingType"],
78
+ ~resolvers=resolversOf([("broken", intResolver(0))]),
79
+ )
80
+ Server.relabelScope(~from="plugin-token-1", ~to_="BrokenPlugin")
81
+ Server.resetScope()
82
+
83
+ switch Server.composeSchema() {
84
+ | _ => Runner.fail("expected composeSchema to throw")
85
+ | exception JsExn(err) => {
86
+ expect(
87
+ exnMessage(err),
88
+ )->toContain(`Plugin "BrokenPlugin" subgraph document is not valid standalone`)
89
+ expect(exnMessage(err))->toContain("MissingType")
90
+ }
88
91
  }
89
- }
90
- })
92
+ },
93
+ )
91
94
 
92
95
  // ── (c) conflicting same-named type across scopes fails the merge ────────────
93
96
 
@@ -109,7 +112,9 @@ testSync("two scopes defining the same type name with conflicting fields fail co
109
112
  switch Server.composeSchema() {
110
113
  | _ => Runner.fail("expected composeSchema to throw")
111
114
  | exception JsExn(err) =>
112
- expect(exnMessage(err))->toContain("Cross-plugin schema merge failed (mirrors AWS MERGE_FAILED)")
115
+ expect(exnMessage(err))->toContain(
116
+ "Cross-plugin schema merge failed (mirrors AWS MERGE_FAILED)",
117
+ )
113
118
  }
114
119
  })
115
120
 
@@ -141,21 +146,26 @@ testSync("reset clears all scope buckets and restores the platform scope", () =>
141
146
 
142
147
  // ── per-scope shared types keep plugin subgraphs standalone-valid ────────────
143
148
 
144
- test("scope missing its own shared-type copy fails standalone even when platform defines it", async () => {
145
- // The platform bucket defines SharedThing; the plugin bucket references it
146
- // without carrying its own copy — standalone-invalid, mirroring an AWS
147
- // source API that leans on another API's types.
148
- Server.registerTypes(~sdlTypes=[sharedTypeSdl]) // platform scope
149
- Server.setScope("Leaner")
150
- Server.registerQueries(
151
- ~sdlFields=[" leanItem: SharedThing"],
152
- ~resolvers=resolversOf([("leanItem", intResolver(3))]),
153
- )
154
- Server.resetScope()
155
-
156
- switch Server.composeSchema() {
157
- | _ => Runner.fail("expected composeSchema to throw")
158
- | exception JsExn(err) =>
159
- expect(exnMessage(err))->toContain(`Plugin "Leaner" subgraph document is not valid standalone`)
160
- }
161
- })
149
+ test(
150
+ "scope missing its own shared-type copy fails standalone even when platform defines it",
151
+ async () => {
152
+ // The platform bucket defines SharedThing; the plugin bucket references it
153
+ // without carrying its own copy — standalone-invalid, mirroring an AWS
154
+ // source API that leans on another API's types.
155
+ Server.registerTypes(~sdlTypes=[sharedTypeSdl]) // platform scope
156
+ Server.setScope("Leaner")
157
+ Server.registerQueries(
158
+ ~sdlFields=[" leanItem: SharedThing"],
159
+ ~resolvers=resolversOf([("leanItem", intResolver(3))]),
160
+ )
161
+ Server.resetScope()
162
+
163
+ switch Server.composeSchema() {
164
+ | _ => Runner.fail("expected composeSchema to throw")
165
+ | exception JsExn(err) =>
166
+ expect(
167
+ exnMessage(err),
168
+ )->toContain(`Plugin "Leaner" subgraph document is not valid standalone`)
169
+ }
170
+ },
171
+ )