@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.
- package/CHANGELOG.md +30 -0
- package/package.json +13 -13
- package/src/BakedManifest.res +5 -6
- package/src/EmitCapabilities.res +3 -1
- package/src/LocalPlatformRegistry.res +3 -1
- package/src/LocalPlatformStart.res +7 -6
- package/src/LocalSeedTarget.res +3 -1
- package/src/Platform.res +291 -215
- package/src/ShellConfig.res +10 -17
- package/src/UiHints.res +8 -14
- package/src/UiSlots.res +10 -17
- package/src/adapter/Api/LocalEvents_Server.res +5 -3
- package/src/adapter/Api/LocalGraphQL_Adapter.res +4 -4
- package/src/adapter/Auth/LocalAuth.res +12 -16
- package/src/adapter/Auth/LocalAuth.res.mjs +2 -1
- package/src/adapter/Auth/UserStore.res +13 -40
- package/src/adapter/Auth/UserStore.res.mjs +11 -67
- package/src/adapter/Backend.res +1 -2
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
- package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
- package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
- package/src/adapter/Counter/LocalCounterHandler.res +1 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
- package/src/adapter/DomainGraphQL_Server.res +35 -34
- package/src/adapter/DomainMCP_Server.res +45 -41
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
- package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
- package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
- package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
- package/src/adapter/LocalBus.res +12 -12
- package/src/adapter/LocalCapabilities.res +6 -0
- package/src/adapter/LocalCapabilities.res.mjs +5 -2
- package/src/adapter/LocalGeocodeResolvers.res +1 -4
- package/src/adapter/LocalSideEffectHandler.res +1 -1
- package/src/adapter/LocalStateChangeDescriptor.res +2 -1
- package/src/adapter/MCP_ServerInstance.res +51 -39
- package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
- package/src/adapter/PlatformGraphQL_Server.res +10 -6
- package/src/adapter/PlatformGraphQL_Server.res.mjs +2 -1
- package/src/adapter/ProjectionCheckpoint.res +8 -1
- package/src/adapter/ProjectionPending.res +1 -3
- package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
- package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
- package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
- package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
- package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
- package/src/adapter/SqliteDriver.res +16 -4
- package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
- package/src/components/Aggregate_Builder.res +6 -1
- package/src/components/Counter_Builder.res +1 -5
- package/src/components/DcbEventLog_Builder.res +5 -3
- package/src/components/ExtensionPoint_Builder.res +13 -15
- package/src/components/InboundTranslationSlice_Builder.res +3 -2
- package/src/components/OutboundTranslationSlice_Builder.res +3 -2
- package/src/components/ReadModel_Builder.res +18 -20
- package/src/components/StateViewSlice_Builder.res +3 -2
- package/src/components/Task_Builder.res +12 -13
- package/src/reset/LocalSeedReset.res +2 -5
- package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
- package/src/test/Mocks/MockEventLogStorage.res +8 -3
- package/src/test/Mocks/MockQueryDbStorage.res +3 -2
- package/src/test/TestRunner.res +1 -1
- package/tests/BakedManifestFilesTest.res +7 -10
- package/tests/LocalEventTapTest.res +26 -21
- package/tests/LocalPlatformStartTest.res +33 -28
- package/tests/PluginEventDecodeTest.res +3 -3
- package/tests/ShellConfigTest.res +30 -21
- package/tests/SplitApiFixtures.res +34 -10
- package/tests/SplitApiTest.res +92 -62
- package/tests/adapter/BackendParityTest.res +180 -130
- package/tests/adapter/CollectNEventsTest.res +6 -4
- package/tests/adapter/CommandAuthorizationTest.res +47 -41
- package/tests/adapter/CommandTopicChannelTest.res +163 -139
- package/tests/adapter/CounterHandlerTest.res +95 -77
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
- package/tests/adapter/DcbEventLogStorageTest.res +264 -207
- package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
- package/tests/adapter/EventCollectorChannelTest.res +153 -124
- package/tests/adapter/EventHistoryResolverTest.res +46 -18
- package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
- package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
- package/tests/adapter/EventTapTest.res +37 -20
- package/tests/adapter/EventTopicPublisherTest.res +74 -50
- package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
- package/tests/adapter/HeartbeatRunnerTest.res +99 -81
- package/tests/adapter/InboundTranslationMutationTest.res +4 -1
- package/tests/adapter/LocalAuthHttpTest.res +79 -85
- package/tests/adapter/LocalAuthLoginTest.res +42 -50
- package/tests/adapter/LocalAuthTest.res +8 -16
- package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
- package/tests/adapter/LocalBusBoundedTest.res +187 -117
- package/tests/adapter/LocalBusPubSubTest.res +118 -76
- package/tests/adapter/LocalDispatchContextTest.res +1 -2
- package/tests/adapter/LocalEvents_ServerTest.res +242 -187
- package/tests/adapter/PgProjectionCatchupTest.res +44 -10
- package/tests/adapter/ProjectionCheckpointTest.res +261 -206
- package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
- package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
- package/tests/adapter/QueryDbListResolverTest.res +225 -208
- package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
- package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
- package/tests/adapter/QueryDbStorageTest.res +163 -117
- package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
- package/tests/adapter/QueryEngineTest.res +179 -126
- package/tests/adapter/ScheduledPublisherTest.res +171 -135
- package/tests/adapter/ServedBucketHttpTest.res +65 -59
- package/tests/adapter/SortKey_FilterTest.res +19 -3
- package/tests/adapter/SqliteDriverTest.res +13 -10
- package/tests/adapter/TaskBucketTest.res +94 -80
- package/tests/components/aggregate/AggregateTest.res +31 -18
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
- package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
- package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
- package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
- package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
- package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
- package/tests/components/commandtopic/CommandTopicTest.res +24 -15
- package/tests/components/counter/CounterFixtures.res +6 -8
- package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
- package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
- package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
- package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
- package/tests/components/dcb/DcbFixtures.res +4 -1
- package/tests/components/dcb/DcbTest.res +9 -6
- package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
- package/tests/components/eventcollector/EventCollectorTest.res +6 -3
- package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
- package/tests/components/eventlog/EventLogConflictTest.res +8 -4
- package/tests/components/eventlog/EventLogFixtures.res +3 -3
- package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
- package/tests/components/eventlog/EventLogStreamTest.res +119 -56
- package/tests/components/eventlog/EventLogTest.res +20 -4
- package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
- package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
- package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
- package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
- package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
- package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
- package/tests/components/heartbeat/HeartbeatTest.res +56 -47
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
- package/tests/components/querydb/QueryDbFixtures.res +1 -5
- package/tests/components/querydb/QueryDbTest.res +2 -4
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
- package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
- package/tests/components/readmodel/ReadModelFixtures.res +8 -8
- package/tests/components/scheduler/SchedulerTest.res +76 -58
- package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
- package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
- package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
- package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
- package/tests/components/task/TaskFixtures.res +5 -5
- package/tests/components/task/TaskTest.res +95 -80
- package/tests/plugin/PluginsProjection_GWT.res +3 -1
- package/tests/plugin/UiFragments_GWT.res +6 -1
- 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,
|
|
44
|
+
~owner=None,
|
|
45
|
+
~opts,
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
// Verify the indexes were created in sqlite_master.
|
|
48
|
-
let listIndexes =
|
|
49
|
-
|
|
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(
|
|
53
|
-
|
|
54
|
-
|
|
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(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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,
|
|
102
|
+
~owner=None,
|
|
103
|
+
~opts,
|
|
99
104
|
)
|
|
100
105
|
|
|
101
|
-
let listSql =
|
|
102
|
-
|
|
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(
|
|
116
|
+
sql->String.includes(
|
|
117
|
+
"json_extract(item, '$.tenantId') || '|' || json_extract(item, '$.ownerId')",
|
|
118
|
+
),
|
|
111
119
|
)->toBe(true)
|
|
112
120
|
})
|
|
113
121
|
|
|
114
|
-
testPromise(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
334
|
-
|
|
377
|
+
let items1 = await ops.loadStream("k")->collect
|
|
378
|
+
expect(items1->Array.length)->toBe(0)
|
|
335
379
|
|
|
336
|
-
|
|
337
|
-
"k"
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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(
|
|
251
|
-
|
|
266
|
+
testPromise(
|
|
267
|
+
"bare page, narrowed",
|
|
268
|
+
() => checkPushed(~label="retired-bare", ~retiredScope={field: "archived", values: None}, []),
|
|
252
269
|
)
|
|
253
|
-
testPromise(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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(
|
|
280
|
-
|
|
281
|
-
|
|
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(
|
|
288
|
-
|
|
289
|
-
|
|
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(
|
|
300
|
-
|
|
328
|
+
testPromise(
|
|
329
|
+
"bare page, scoped",
|
|
330
|
+
() => checkPushed(~label="owner-bare", ~ownerScope=("owner", "u-a"), []),
|
|
301
331
|
)
|
|
302
|
-
testPromise(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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(
|
|
320
|
-
|
|
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(
|
|
327
|
-
|
|
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(
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
"
|
|
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(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
})
|