@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
@@ -41,29 +41,33 @@ describe("QueryDbStorage_Sqlite — GSI", () => {
41
41
  ~indexes=[byOwnerIdx, byOwnerCreatedIdx],
42
42
  ~api=(),
43
43
  ~apiRole=(),
44
- ~owner=None, ~opts,
44
+ ~owner=None,
45
+ ~opts,
45
46
  )
46
47
 
47
48
  // Verify the indexes were created in sqlite_master.
48
- let listIndexes = DbProvider.db->SqliteDriver.prepare(
49
- "SELECT name, sql FROM sqlite_master WHERE type='index' AND tbl_name='qdb_orders' ORDER BY name",
50
- )
49
+ let listIndexes =
50
+ DbProvider.db->SqliteDriver.prepare(
51
+ "SELECT name, sql FROM sqlite_master WHERE type='index' AND tbl_name='qdb_orders' ORDER BY name",
52
+ )
51
53
  let rows = listIndexes->SqliteDriver.all([])
52
- let names = rows->Array.map(row =>
53
- switch row->Dict.get("name") {
54
- | Some(JSON.String(s)) => s
55
- | _ => ""
56
- }
54
+ let names = rows->Array.map(
55
+ row =>
56
+ switch row->Dict.get("name") {
57
+ | Some(JSON.String(s)) => s
58
+ | _ => ""
59
+ },
57
60
  )
58
61
  expect(names->Array.includes("idx_qdb_orders_ByOwner"))->toBe(true)
59
62
  expect(names->Array.includes("idx_qdb_orders_ByOwnerCreated"))->toBe(true)
60
63
 
61
64
  // Confirm the index definition references json_extract on the configured field.
62
- let firstSql = switch rows->Array.find(row =>
63
- switch row->Dict.get("name") {
64
- | Some(JSON.String(s)) => s == "idx_qdb_orders_ByOwner"
65
- | _ => false
66
- }
65
+ let firstSql = switch rows->Array.find(
66
+ row =>
67
+ switch row->Dict.get("name") {
68
+ | Some(JSON.String(s)) => s == "idx_qdb_orders_ByOwner"
69
+ | _ => false
70
+ },
67
71
  ) {
68
72
  | Some(row) =>
69
73
  switch row->Dict.get("sql") {
@@ -95,56 +99,66 @@ describe("QueryDbStorage_Sqlite — GSI", () => {
95
99
  ~indexes=[compositeIdx],
96
100
  ~api=(),
97
101
  ~apiRole=(),
98
- ~owner=None, ~opts,
102
+ ~owner=None,
103
+ ~opts,
99
104
  )
100
105
 
101
- let listSql = DbProvider.db->SqliteDriver.prepare(
102
- "SELECT sql FROM sqlite_master WHERE type='index' AND name='idx_qdb_composite_ByTenantOwner'",
103
- )
106
+ let listSql =
107
+ DbProvider.db->SqliteDriver.prepare(
108
+ "SELECT sql FROM sqlite_master WHERE type='index' AND name='idx_qdb_composite_ByTenantOwner'",
109
+ )
104
110
  let row = listSql->SqliteDriver.get([])->Option.getOrThrow
105
111
  let sql = switch row->Dict.get("sql") {
106
112
  | Some(JSON.String(s)) => s
107
113
  | _ => ""
108
114
  }
109
115
  expect(
110
- sql->String.includes("json_extract(item, '$.tenantId') || '|' || json_extract(item, '$.ownerId')"),
116
+ sql->String.includes(
117
+ "json_extract(item, '$.tenantId') || '|' || json_extract(item, '$.ownerId')",
118
+ ),
111
119
  )->toBe(true)
112
120
  })
113
121
 
114
- testPromise("index name is sanitised — special characters in index name become underscores", async () => {
115
- module TestBus = LocalBus.Make()
116
- module DbProvider = {
117
- let db = openFreshDb()
118
- }
119
- module Storage = QueryDbStorage_Sqlite.Make(TestBus, DbProvider)
122
+ testPromise(
123
+ "index name is sanitised — special characters in index name become underscores",
124
+ async () => {
125
+ module TestBus = LocalBus.Make()
126
+ module DbProvider = {
127
+ let db = openFreshDb()
128
+ }
129
+ module Storage = QueryDbStorage_Sqlite.Make(TestBus, DbProvider)
120
130
 
121
- let weirdIdx: Reventless.ReadModel.indexConfig = {
122
- index: "by-thing.v2",
123
- type_: "S",
124
- idField: "thing",
125
- projectionType: ALL,
126
- }
127
- let _ = Storage.make(
128
- ~name="rm",
129
- ~indexes=[weirdIdx],
130
- ~api=(),
131
- ~apiRole=(),
132
- ~owner=None, ~opts,
133
- )
134
- let listIndexes = DbProvider.db->SqliteDriver.prepare(
135
- "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='qdb_rm'",
136
- )
137
- let names =
138
- listIndexes
139
- ->SqliteDriver.all([])
140
- ->Array.map(row =>
141
- switch row->Dict.get("name") {
142
- | Some(JSON.String(s)) => s
143
- | _ => ""
144
- }
131
+ let weirdIdx: Reventless.ReadModel.indexConfig = {
132
+ index: "by-thing.v2",
133
+ type_: "S",
134
+ idField: "thing",
135
+ projectionType: ALL,
136
+ }
137
+ let _ = Storage.make(
138
+ ~name="rm",
139
+ ~indexes=[weirdIdx],
140
+ ~api=(),
141
+ ~apiRole=(),
142
+ ~owner=None,
143
+ ~opts,
145
144
  )
146
- expect(names->Array.includes("idx_qdb_rm_by_thing_v2"))->toBe(true)
147
- })
145
+ let listIndexes =
146
+ DbProvider.db->SqliteDriver.prepare(
147
+ "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='qdb_rm'",
148
+ )
149
+ let names =
150
+ listIndexes
151
+ ->SqliteDriver.all([])
152
+ ->Array.map(
153
+ row =>
154
+ switch row->Dict.get("name") {
155
+ | Some(JSON.String(s)) => s
156
+ | _ => ""
157
+ },
158
+ )
159
+ expect(names->Array.includes("idx_qdb_rm_by_thing_v2"))->toBe(true)
160
+ },
161
+ )
148
162
  })
149
163
 
150
164
  describe("QueryDb — indexed equality lookup (B4 push-down)", () => {
@@ -166,7 +180,14 @@ describe("QueryDb — indexed equality lookup (B4 push-down)", () => {
166
180
  let db = openFreshDb()
167
181
  }
168
182
  module Storage = QueryDbStorage_Sqlite.Make(TestBus, DbProvider)
169
- let s = Storage.make(~name="orders", ~indexes=[byOwner], ~api=(), ~apiRole=(), ~owner=None, ~opts)
183
+ let s = Storage.make(
184
+ ~name="orders",
185
+ ~indexes=[byOwner],
186
+ ~api=(),
187
+ ~apiRole=(),
188
+ ~owner=None,
189
+ ~opts,
190
+ )
170
191
  let ops = await s.operations->TestRunner.resolve
171
192
  let _ = await ops.save("k1", item("o1"), ReventlessCore.QueryDb.Any, None)
172
193
  let _ = await ops.save("k2", item("o2"), ReventlessCore.QueryDb.Any, None)
@@ -184,7 +205,14 @@ describe("QueryDb — indexed equality lookup (B4 push-down)", () => {
184
205
  let db = openFreshDb()
185
206
  }
186
207
  module Storage = QueryDbStorage_Sqlite.Make(TestBus, DbProvider)
187
- let s = Storage.make(~name="orders", ~indexes=[byOwner], ~api=(), ~apiRole=(), ~owner=None, ~opts)
208
+ let s = Storage.make(
209
+ ~name="orders",
210
+ ~indexes=[byOwner],
211
+ ~api=(),
212
+ ~apiRole=(),
213
+ ~owner=None,
214
+ ~opts,
215
+ )
188
216
  let ops = await s.operations->TestRunner.resolve
189
217
  let aMinuteAgo = Float.toInt(Date.now() /. 1000.0) - 60
190
218
  let _ = await ops.save("k1", item("o1"), ReventlessCore.QueryDb.Any, None)
@@ -196,7 +224,14 @@ describe("QueryDb — indexed equality lookup (B4 push-down)", () => {
196
224
  testPromise("in-memory lookup matches the sqlite result (backend parity)", async () => {
197
225
  module TestBus = LocalBus.Make()
198
226
  module Storage = QueryDbStorage_InMemory.Make(TestBus)
199
- let s = Storage.make(~name="orders", ~indexes=[byOwner], ~api=(), ~apiRole=(), ~owner=None, ~opts)
227
+ let s = Storage.make(
228
+ ~name="orders",
229
+ ~indexes=[byOwner],
230
+ ~api=(),
231
+ ~apiRole=(),
232
+ ~owner=None,
233
+ ~opts,
234
+ )
200
235
  let ops = await s.operations->TestRunner.resolve
201
236
  let _ = await ops.save("k1", item("o1"), ReventlessCore.QueryDb.Any, None)
202
237
  let _ = await ops.save("k2", item("o2"), ReventlessCore.QueryDb.Any, None)
@@ -312,35 +347,40 @@ describe("QueryDbStorage_Sqlite — TTL", () => {
312
347
  }
313
348
  })
314
349
 
315
- testPromise("overwriting an expired row with a non-expired one makes it visible again", async () => {
316
- module TestBus = LocalBus.Make()
317
- module DbProvider = {
318
- let db = openFreshDb()
319
- }
320
- module Storage = QueryDbStorage_Sqlite.Make(TestBus, DbProvider)
321
-
322
- let s = Storage.make(~name="overwrite-ttl", ~indexes=[], ~api=(), ~apiRole=(), ~owner=None, ~opts)
323
- let ops = await s.operations->TestRunner.resolve
324
-
325
- let aMinuteAgo = Float.toInt(Date.now() /. 1000.0) - 60
326
- let _ = await ops.save(
327
- "k",
328
- JSON.Encode.string("expired"),
329
- ReventlessCore.QueryDb.Any,
330
- Some(aMinuteAgo),
331
- )
350
+ testPromise(
351
+ "overwriting an expired row with a non-expired one makes it visible again",
352
+ async () => {
353
+ module TestBus = LocalBus.Make()
354
+ module DbProvider = {
355
+ let db = openFreshDb()
356
+ }
357
+ module Storage = QueryDbStorage_Sqlite.Make(TestBus, DbProvider)
358
+
359
+ let s = Storage.make(
360
+ ~name="overwrite-ttl",
361
+ ~indexes=[],
362
+ ~api=(),
363
+ ~apiRole=(),
364
+ ~owner=None,
365
+ ~opts,
366
+ )
367
+ let ops = await s.operations->TestRunner.resolve
368
+
369
+ let aMinuteAgo = Float.toInt(Date.now() /. 1000.0) - 60
370
+ let _ = await ops.save(
371
+ "k",
372
+ JSON.Encode.string("expired"),
373
+ ReventlessCore.QueryDb.Any,
374
+ Some(aMinuteAgo),
375
+ )
332
376
 
333
- let items1 = await ops.loadStream("k")->collect
334
- expect(items1->Array.length)->toBe(0)
377
+ let items1 = await ops.loadStream("k")->collect
378
+ expect(items1->Array.length)->toBe(0)
335
379
 
336
- let _ = await ops.save(
337
- "k",
338
- JSON.Encode.string("revived"),
339
- ReventlessCore.QueryDb.Any,
340
- None,
341
- )
342
- let items2 = await ops.loadStream("k")->collect
343
- expect(items2->Array.length)->toBe(1)
344
- expect(items2->Array.getUnsafe(0))->toEqual(JSON.Encode.string("revived"))
345
- })
380
+ let _ = await ops.save("k", JSON.Encode.string("revived"), ReventlessCore.QueryDb.Any, None)
381
+ let items2 = await ops.loadStream("k")->collect
382
+ expect(items2->Array.length)->toBe(1)
383
+ expect(items2->Array.getUnsafe(0))->toEqual(JSON.Encode.string("revived"))
384
+ },
385
+ )
346
386
  })
@@ -154,13 +154,20 @@ let checkPushed = async (~label, ~ownerScope=?, ~retiredScope=?, args) => {
154
154
  // Assert the push-down declines this shape (resolver falls back to the spec).
155
155
  let checkFallback = async args => {
156
156
  let s = await build()
157
- expect(s.listPage(~argsDict=argsOf(args), ~capability, ~labelField="name")->Option.isSome)->toBe(false)
157
+ expect(s.listPage(~argsDict=argsOf(args), ~capability, ~labelField="name")->Option.isSome)->toBe(
158
+ false,
159
+ )
158
160
  }
159
161
 
160
162
  // The ids a retirement-narrowed read returns, from the push-down.
161
163
  let liveIds = async args => {
162
164
  let s = await build()
163
- switch s.listPage(~argsDict=argsOf(args), ~capability, ~labelField="name", ~retiredScope={field: "archived", values: None}) {
165
+ switch s.listPage(
166
+ ~argsDict=argsOf(args),
167
+ ~capability,
168
+ ~labelField="name",
169
+ ~retiredScope={field: "archived", values: None},
170
+ ) {
164
171
  | Some(conn) => (conn->norm).edges->Array.map(e => e.id)
165
172
  | None => []
166
173
  }
@@ -184,7 +191,10 @@ describe("QueryDb list push-down parity (SQLite ≡ QueryDbListQuery spec)", ()
184
191
  testPromise("bare page", () => checkPushed(~label="bare", []))
185
192
  testPromise("first:2", () => checkPushed(~label="first", [("first", JSON.Encode.int(2))]))
186
193
  testPromise("first:2 + after", () =>
187
- checkPushed(~label="after", [("first", JSON.Encode.int(2)), ("after", JSON.Encode.string(cur("p-2")))])
194
+ checkPushed(
195
+ ~label="after",
196
+ [("first", JSON.Encode.int(2)), ("after", JSON.Encode.string(cur("p-2")))],
197
+ )
188
198
  )
189
199
  testPromise("statusEq active", () =>
190
200
  checkPushed(~label="eq", [("filter", filterOf([("statusEq", JSON.Encode.string("active"))]))])
@@ -192,7 +202,10 @@ describe("QueryDb list push-down parity (SQLite ≡ QueryDbListQuery spec)", ()
192
202
  testPromise("statusEq active + first:2", () =>
193
203
  checkPushed(
194
204
  ~label="eq+first",
195
- [("filter", filterOf([("statusEq", JSON.Encode.string("active"))])), ("first", JSON.Encode.int(2))],
205
+ [
206
+ ("filter", filterOf([("statusEq", JSON.Encode.string("active"))])),
207
+ ("first", JSON.Encode.int(2)),
208
+ ],
196
209
  )
197
210
  )
198
211
  testPromise("orderBy name ASC", () =>
@@ -219,7 +232,12 @@ describe("QueryDb list push-down parity (SQLite ≡ QueryDbListQuery spec)", ()
219
232
  testPromise("qty range From/To (numeric-as-string)", () =>
220
233
  checkPushed(
221
234
  ~label="range",
222
- [("filter", filterOf([("qtyFrom", JSON.Encode.string("2")), ("qtyTo", JSON.Encode.string("4"))]))],
235
+ [
236
+ (
237
+ "filter",
238
+ filterOf([("qtyFrom", JSON.Encode.string("2")), ("qtyTo", JSON.Encode.string("4"))]),
239
+ ),
240
+ ],
223
241
  )
224
242
  )
225
243
  testPromise("orderBy qty ASC (numeric-as-string sort)", () =>
@@ -234,9 +252,7 @@ describe("QueryDb list push-down parity (SQLite ≡ QueryDbListQuery spec)", ()
234
252
  checkFallback([("filter", filterOf([("searchPrefix", JSON.Encode.string("A"))]))])
235
253
  )
236
254
  testPromise("ids → fallback", () =>
237
- checkFallback([
238
- ("filter", filterOf([("ids", JSON.Encode.array([JSON.Encode.string("p-1")]))])),
239
- ])
255
+ checkFallback([("filter", filterOf([("ids", JSON.Encode.array([JSON.Encode.string("p-1")]))]))])
240
256
  )
241
257
  testPromise("backward (last/before) → fallback", () =>
242
258
  checkFallback([("last", JSON.Encode.int(2)), ("before", JSON.Encode.string(cur("p-4")))])
@@ -247,47 +263,60 @@ describe("QueryDb list push-down parity (SQLite ≡ QueryDbListQuery spec)", ()
247
263
  // deployment takes and the spec is the path the tests reach, so a predicate
248
264
  // implemented in one of them is a hole every fallback-based test calls green.
249
265
  describe("retirement narrowing", () => {
250
- testPromise("bare page, narrowed", () =>
251
- checkPushed(~label="retired-bare", ~retiredScope={field: "archived", values: None}, [])
266
+ testPromise(
267
+ "bare page, narrowed",
268
+ () => checkPushed(~label="retired-bare", ~retiredScope={field: "archived", values: None}, []),
252
269
  )
253
- testPromise("narrowed + orderBy", () =>
254
- checkPushed(
255
- ~label="retired-order",
256
- ~retiredScope={field: "archived", values: None},
257
- [("orderBy", orderBy("name", "DESC"))],
258
- )
270
+ testPromise(
271
+ "narrowed + orderBy",
272
+ () =>
273
+ checkPushed(
274
+ ~label="retired-order",
275
+ ~retiredScope={field: "archived", values: None},
276
+ [("orderBy", orderBy("name", "DESC"))],
277
+ ),
259
278
  )
260
- testPromise("narrowed + the caller's own filter", () =>
261
- checkPushed(
262
- ~label="retired-filter",
263
- ~retiredScope={field: "archived", values: None},
264
- [("filter", filterOf([("statusEq", JSON.Encode.string("active"))]))],
265
- )
279
+ testPromise(
280
+ "narrowed + the caller's own filter",
281
+ () =>
282
+ checkPushed(
283
+ ~label="retired-filter",
284
+ ~retiredScope={field: "archived", values: None},
285
+ [("filter", filterOf([("statusEq", JSON.Encode.string("active"))]))],
286
+ ),
266
287
  )
267
- testPromise("narrowed + owner-scoped together", () =>
268
- checkPushed(
269
- ~label="retired-owner",
270
- ~ownerScope=("owner", "u-a"),
271
- ~retiredScope={field: "archived", values: None},
272
- [],
273
- )
288
+ testPromise(
289
+ "narrowed + owner-scoped together",
290
+ () =>
291
+ checkPushed(
292
+ ~label="retired-owner",
293
+ ~ownerScope=("owner", "u-a"),
294
+ ~retiredScope={field: "archived", values: None},
295
+ [],
296
+ ),
274
297
  )
275
298
 
276
299
  // The rows, not just the parity: p-2 and p-4 are archived, p-5 carries no
277
300
  // flag at all. Parity alone would pass if BOTH implementations were wrong
278
301
  // the same way.
279
- testPromise("drops the archived rows and keeps the one with no flag", async () => {
280
- let ids = await liveIds([])
281
- expect(ids)->toEqual(["p-1", "p-3", "p-5"])
282
- })
302
+ testPromise(
303
+ "drops the archived rows and keeps the one with no flag",
304
+ async () => {
305
+ let ids = await liveIds([])
306
+ expect(ids)->toEqual(["p-1", "p-3", "p-5"])
307
+ },
308
+ )
283
309
 
284
310
  // The reason the predicate is pushed into the SQL rather than applied to the
285
311
  // page: `first: 2` must yield two LIVE rows, not two rows of which some were
286
312
  // filtered away afterwards.
287
- testPromise("a page of first:2 is two live rows, not two rows minus the archived", async () => {
288
- let ids = await liveIds([("first", JSON.Encode.int(2))])
289
- expect(ids)->toEqual(["p-1", "p-3"])
290
- })
313
+ testPromise(
314
+ "a page of first:2 is two live rows, not two rows minus the archived",
315
+ async () => {
316
+ let ids = await liveIds([("first", JSON.Encode.int(2))])
317
+ expect(ids)->toEqual(["p-1", "p-3"])
318
+ },
319
+ )
291
320
  })
292
321
 
293
322
  // ── Owner scoping ─────────────────────────────────────────────────────────
@@ -296,55 +325,66 @@ describe("QueryDb list push-down parity (SQLite ≡ QueryDbListQuery spec)", ()
296
325
  // easily reach. A predicate implemented in only one of them is a hole that
297
326
  // every fallback-based test would still call green.
298
327
  describe("owner scoping", () => {
299
- testPromise("bare page, scoped", () =>
300
- checkPushed(~label="owner-bare", ~ownerScope=("owner", "u-a"), [])
328
+ testPromise(
329
+ "bare page, scoped",
330
+ () => checkPushed(~label="owner-bare", ~ownerScope=("owner", "u-a"), []),
301
331
  )
302
- testPromise("scoped + orderBy", () =>
303
- checkPushed(
304
- ~label="owner-order",
305
- ~ownerScope=("owner", "u-a"),
306
- [("orderBy", orderBy("name", "DESC"))],
307
- )
332
+ testPromise(
333
+ "scoped + orderBy",
334
+ () =>
335
+ checkPushed(
336
+ ~label="owner-order",
337
+ ~ownerScope=("owner", "u-a"),
338
+ [("orderBy", orderBy("name", "DESC"))],
339
+ ),
308
340
  )
309
341
  // The client's own filter must survive alongside the scope, not be replaced
310
342
  // by it — a scoped read is still a filtered read.
311
- testPromise("scoped + client filter compose", () =>
312
- checkPushed(
313
- ~label="owner-and-filter",
314
- ~ownerScope=("owner", "u-a"),
315
- [("filter", filterOf([("statusEq", JSON.Encode.string("inactive"))]))],
316
- )
343
+ testPromise(
344
+ "scoped + client filter compose",
345
+ () =>
346
+ checkPushed(
347
+ ~label="owner-and-filter",
348
+ ~ownerScope=("owner", "u-a"),
349
+ [("filter", filterOf([("statusEq", JSON.Encode.string("inactive"))]))],
350
+ ),
317
351
  )
318
352
 
319
- testPromise("a scoped read returns exactly that owner's rows", async () =>
320
- expect(await scopedIds(~owner="u-a", []))->toEqual(["p-1", "p-3", "p-5"])
353
+ testPromise(
354
+ "a scoped read returns exactly that owner's rows",
355
+ async () => expect(await scopedIds(~owner="u-a", []))->toEqual(["p-1", "p-3", "p-5"]),
321
356
  )
322
357
 
323
358
  // The control: a different owner sees a different, also non-empty, set. An
324
359
  // implementation that scoped to nothing would pass an "is not everything"
325
360
  // assertion and fail this one.
326
- testPromise("a second owner sees their own rows, not the first's", async () =>
327
- expect(await scopedIds(~owner="u-b", []))->toEqual(["p-2"])
361
+ testPromise(
362
+ "a second owner sees their own rows, not the first's",
363
+ async () => expect(await scopedIds(~owner="u-b", []))->toEqual(["p-2"]),
328
364
  )
329
365
 
330
366
  // The case that catches a predicate applied AFTER the page rather than
331
367
  // inside the SQL: u-a owns 3 of 5 rows, so a LIMIT 2 taken before scoping
332
368
  // would return p-1 alone (p-2 belongs to u-b and would be dropped), not the
333
369
  // two rows actually asked for.
334
- testPromise("paging a scoped read fills each page from owned rows only", async () =>
335
- expect(await scopedIds(~owner="u-a", [("first", JSON.Encode.int(2))]))->toEqual([
336
- "p-1",
337
- "p-3",
338
- ])
370
+ testPromise(
371
+ "paging a scoped read fills each page from owned rows only",
372
+ async () =>
373
+ expect(await scopedIds(~owner="u-a", [("first", JSON.Encode.int(2))]))->toEqual([
374
+ "p-1",
375
+ "p-3",
376
+ ]),
339
377
  )
340
378
 
341
- testPromise("the second page continues the scoped sequence", async () =>
342
- expect(
343
- await scopedIds(
344
- ~owner="u-a",
345
- [("first", JSON.Encode.int(2)), ("after", JSON.Encode.string(cur("p-3")))],
346
- ),
347
- )->toEqual(["p-5"])
379
+ testPromise(
380
+ "the second page continues the scoped sequence",
381
+ async () =>
382
+ expect(
383
+ await scopedIds(
384
+ ~owner="u-a",
385
+ [("first", JSON.Encode.int(2)), ("after", JSON.Encode.string(cur("p-3")))],
386
+ ),
387
+ )->toEqual(["p-5"]),
348
388
  )
349
389
  })
350
390
  })