@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.
- package/CHANGELOG.md +8 -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 +11 -15
- package/src/adapter/Auth/UserStore.res +5 -7
- 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.mjs +1 -1
- 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 +9 -5
- 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
|
@@ -57,13 +57,17 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
57
57
|
// -- Module-level server and relay refs ------------------------------------
|
|
58
58
|
// Set by Platform.res before component construction runs so the make function
|
|
59
59
|
// can pick up the correct target server and relay support (mirrors AWS ~api pattern).
|
|
60
|
-
let serverRef: ref<ReventlessGraphqlServer.GraphQL_ServerInstance.t> = ref(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
let serverRef: ref<ReventlessGraphqlServer.GraphQL_ServerInstance.t> = ref(
|
|
61
|
+
DomainGraphQL_Server.asInterface,
|
|
62
|
+
)
|
|
63
|
+
let relayRef: ref<option<relaySupport>> = ref(
|
|
64
|
+
Some({
|
|
65
|
+
encodeGlobalId: DomainGraphQL_Server.encodeGlobalId,
|
|
66
|
+
registerNodeType: DomainGraphQL_Server.registerNodeType,
|
|
67
|
+
registerNodeResolverCallback: DomainGraphQL_Server.registerNodeResolverCallback,
|
|
68
|
+
nodeTypeRegistry: DomainGraphQL_Server.nodeTypeRegistry,
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
67
71
|
|
|
68
72
|
let make: QueryDb_Adapter.resolversMaker<unit, unit> = (
|
|
69
73
|
~name,
|
|
@@ -101,11 +105,10 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
101
105
|
}
|
|
102
106
|
switch Bus.getQueryDb(queryDbName) {
|
|
103
107
|
| Some(ops) =>
|
|
104
|
-
let items =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
->Effect.runPromise
|
|
108
|
+
let items = await ops.loadStream(localId)
|
|
109
|
+
->Stream.runCollect
|
|
110
|
+
->Effect.catchAll(_ => Effect.succeed([]))
|
|
111
|
+
->Effect.runPromise
|
|
109
112
|
switch items->Array.get(0) {
|
|
110
113
|
| Some(item) =>
|
|
111
114
|
// Copied: `JSON.Decode.object` hands back the stored object itself, so
|
|
@@ -125,6 +128,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
125
128
|
|
|
126
129
|
let runInterceptor = async (~ctx, ~args): QueryDb_Callback.interceptResult => {
|
|
127
130
|
let identity = extractIdentity(ctx)
|
|
131
|
+
|
|
128
132
|
// Spec-level authorization runs first; failures short-circuit before
|
|
129
133
|
// the user-supplied interceptor (mirrors mutation enforcement).
|
|
130
134
|
if !Reventless.Authorization.isAllowed(authorization, identity) {
|
|
@@ -291,12 +295,20 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
291
295
|
} else {
|
|
292
296
|
` ${singleQueryName}: ${returnTypeName}`
|
|
293
297
|
}
|
|
294
|
-
let byIdResolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
298
|
+
let byIdResolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
299
|
+
_root,
|
|
300
|
+
args,
|
|
301
|
+
ctx,
|
|
302
|
+
) => {
|
|
295
303
|
switch await runInterceptor(~ctx, ~args) {
|
|
296
304
|
| Deny(_) => JSON.Encode.null
|
|
297
305
|
| Allow =>
|
|
298
306
|
let id =
|
|
299
|
-
args
|
|
307
|
+
args
|
|
308
|
+
->JSON.Decode.object
|
|
309
|
+
->Option.flatMap(d => d->Dict.get("id"))
|
|
310
|
+
->Option.flatMap(JSON.Decode.string)
|
|
311
|
+
->Option.getOr("")
|
|
300
312
|
switch Bus.getQueryDb(name) {
|
|
301
313
|
| Some(ops) =>
|
|
302
314
|
let load = key =>
|
|
@@ -309,10 +321,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
309
321
|
// as one. Retried rather than decoded up front: the raw key is what this
|
|
310
322
|
// door has always taken, and a key that merely looks like base64 must
|
|
311
323
|
// keep resolving to its own row.
|
|
312
|
-
let (resolvedKey, items) = switch (
|
|
313
|
-
firstAttempt->Array.get(0),
|
|
314
|
-
Api_Ids.alternateKey(id),
|
|
315
|
-
) {
|
|
324
|
+
let (resolvedKey, items) = switch (firstAttempt->Array.get(0), Api_Ids.alternateKey(id)) {
|
|
316
325
|
| (None, Some(localId)) => (localId, await load(localId))
|
|
317
326
|
| _ => (id, firstAttempt)
|
|
318
327
|
}
|
|
@@ -320,8 +329,8 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
320
329
|
// A row the caller does not own answers as though it were not there.
|
|
321
330
|
// Distinguishing "not yours" from "not found" here would turn this door
|
|
322
331
|
// into an oracle for which ids exist.
|
|
323
|
-
| Some(item)
|
|
324
|
-
JSON.Encode.null
|
|
332
|
+
| Some(item)
|
|
333
|
+
if !ownerAllows(~ctx, item) || !retiredAllows(~ctx, ~args, item) => JSON.Encode.null
|
|
325
334
|
| Some(item) =>
|
|
326
335
|
if includeIdParam {
|
|
327
336
|
// Copied — `JSON.Decode.object` hands back the stored object itself,
|
|
@@ -349,61 +358,74 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
349
358
|
// ids drop out of the response (no cardinality preservation), matching
|
|
350
359
|
// BatchGetItem semantics.
|
|
351
360
|
let byIdsSdl = if includeIdParam && subIdField === None {
|
|
352
|
-
[
|
|
361
|
+
[
|
|
362
|
+
GraphQL_FragmentGenerator.deriveByIdsQueryField(
|
|
363
|
+
~listFieldName=listQueryName,
|
|
364
|
+
~returnTypeName,
|
|
365
|
+
),
|
|
366
|
+
]
|
|
353
367
|
} else {
|
|
354
368
|
[]
|
|
355
369
|
}
|
|
356
|
-
let byIdsResolverEntry: option<(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
370
|
+
let byIdsResolverEntry: option<(
|
|
371
|
+
string,
|
|
372
|
+
ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn,
|
|
373
|
+
)> = if includeIdParam && subIdField === None {
|
|
374
|
+
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
375
|
+
_root,
|
|
376
|
+
args,
|
|
377
|
+
ctx,
|
|
378
|
+
) => {
|
|
379
|
+
switch await runInterceptor(~ctx, ~args) {
|
|
380
|
+
| Deny(_) => []->JSON.Encode.array
|
|
381
|
+
| Allow =>
|
|
382
|
+
let ids =
|
|
383
|
+
args
|
|
384
|
+
->JSON.Decode.object
|
|
385
|
+
->Option.flatMap(d => d->Dict.get("ids"))
|
|
386
|
+
->Option.flatMap(JSON.Decode.array)
|
|
387
|
+
->Option.getOr([])
|
|
388
|
+
->Array.filterMap(JSON.Decode.string)
|
|
389
|
+
switch Bus.getQueryDb(name) {
|
|
390
|
+
| Some(ops) =>
|
|
391
|
+
let load = key =>
|
|
392
|
+
ops.loadStream(key)
|
|
393
|
+
->Stream.runCollect
|
|
394
|
+
->Effect.catchAll(_ => Effect.succeed([]))
|
|
395
|
+
->Effect.runPromise
|
|
396
|
+
// Same either-form rule as the single-id door: raw key first, the
|
|
397
|
+
// key inside a Relay global id only on a miss.
|
|
398
|
+
let loaded = await ids
|
|
399
|
+
->Array.map(async id =>
|
|
400
|
+
switch (await load(id))->Array.get(0) {
|
|
401
|
+
| Some(item) => (id, Some(item))
|
|
402
|
+
| None =>
|
|
403
|
+
switch Api_Ids.alternateKey(id) {
|
|
404
|
+
| Some(localId) => (localId, (await load(localId))->Array.get(0))
|
|
405
|
+
| None => (id, None)
|
|
386
406
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
407
|
+
}
|
|
408
|
+
)
|
|
409
|
+
->Promise.all
|
|
410
|
+
loaded
|
|
411
|
+
->Array.filterMap(((id, opt)) =>
|
|
412
|
+
opt
|
|
413
|
+
->Option.filter(item => ownerAllows(~ctx, item) && retiredAllows(~ctx, ~args, item))
|
|
414
|
+
->Option.map(item => {
|
|
415
|
+
let obj = item->JSON.Decode.object->Option.mapOr(Dict.make(), Dict.copy)
|
|
416
|
+
obj->Dict.set("id", JSON.Encode.string(id))
|
|
417
|
+
JSON.Encode.object(obj)
|
|
418
|
+
})
|
|
419
|
+
)
|
|
420
|
+
->JSON.Encode.array
|
|
421
|
+
| None => []->JSON.Encode.array
|
|
401
422
|
}
|
|
402
423
|
}
|
|
403
|
-
Some((listQueryName ++ "ByIds", resolver))
|
|
404
|
-
} else {
|
|
405
|
-
None
|
|
406
424
|
}
|
|
425
|
+
Some((listQueryName ++ "ByIds", resolver))
|
|
426
|
+
} else {
|
|
427
|
+
None
|
|
428
|
+
}
|
|
407
429
|
|
|
408
430
|
// Look up labelField from registry (Phase 3). Used by list-query filter.search /
|
|
409
431
|
// searchPrefix to target the entity's human-readable column without per-entity
|
|
@@ -426,120 +448,126 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
426
448
|
// else. One predicate answers "is this row still on offer", the other "is this
|
|
427
449
|
// row yours", and only the first is what an archive withdraws.
|
|
428
450
|
let refsSdl = if includeIdParam && subIdField === None {
|
|
429
|
-
[
|
|
451
|
+
[
|
|
452
|
+
GraphQL_FragmentGenerator.deriveRefsQueryField(
|
|
453
|
+
~listFieldName=listQueryName,
|
|
454
|
+
~returnTypeName,
|
|
455
|
+
),
|
|
456
|
+
]
|
|
430
457
|
} else {
|
|
431
458
|
[]
|
|
432
459
|
}
|
|
433
|
-
let refsResolverEntry: option<(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
460
|
+
let refsResolverEntry: option<(
|
|
461
|
+
string,
|
|
462
|
+
ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn,
|
|
463
|
+
)> = if includeIdParam && subIdField === None {
|
|
464
|
+
server.registerTypes(~sdlTypes=[GraphQL_FragmentGenerator.deriveRefTypeSdl(~returnTypeName)])
|
|
465
|
+
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
466
|
+
_root,
|
|
467
|
+
args,
|
|
468
|
+
ctx,
|
|
469
|
+
) => {
|
|
470
|
+
switch await runInterceptor(~ctx, ~args) {
|
|
471
|
+
| Deny(_) => []->JSON.Encode.array
|
|
472
|
+
| Allow =>
|
|
473
|
+
let ids =
|
|
474
|
+
args
|
|
475
|
+
->JSON.Decode.object
|
|
476
|
+
->Option.flatMap(d => d->Dict.get("ids"))
|
|
477
|
+
->Option.flatMap(JSON.Decode.array)
|
|
478
|
+
->Option.getOr([])
|
|
479
|
+
->Array.filterMap(JSON.Decode.string)
|
|
480
|
+
let spec = retiredSpecOf()
|
|
481
|
+
let namesRetired = spec->Option.mapOr(false, r => r.namedWhenRetired)
|
|
482
|
+
// The row's own retirement, read with the same `isRetiredValue` every
|
|
483
|
+
// other door narrows by — so "retired" here and "withheld" there are
|
|
484
|
+
// one answer, not two that drift.
|
|
485
|
+
let retirementOf = (item: JSON.t) =>
|
|
486
|
+
switch spec {
|
|
487
|
+
| None => (false, None)
|
|
488
|
+
| Some(r) =>
|
|
489
|
+
let cell = item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(r.field))
|
|
490
|
+
let scope: Reventless.OwnerScope.retiredScope = {field: r.field, values: r.values}
|
|
491
|
+
let retired = scope->Reventless.OwnerScope.isRetiredValue(cell)
|
|
492
|
+
(
|
|
493
|
+
retired,
|
|
494
|
+
// The state that retired it, and only that. A live row reports
|
|
495
|
+
// none — this door names rows, it does not publish a lifecycle
|
|
496
|
+
// column to callers the list withholds. Nor does the boolean
|
|
497
|
+
// form, where the field is the state and `retired` said it.
|
|
498
|
+
retired && r.values->Option.isSome
|
|
499
|
+
? cell->Option.flatMap(JSON.Decode.string)
|
|
500
|
+
: None,
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
switch Bus.getQueryDb(name) {
|
|
504
|
+
| Some(ops) =>
|
|
505
|
+
let load = key =>
|
|
506
|
+
ops.loadStream(key)
|
|
507
|
+
->Stream.runCollect
|
|
508
|
+
->Effect.catchAll(_ => Effect.succeed([]))
|
|
509
|
+
->Effect.runPromise
|
|
510
|
+
let loaded = await ids
|
|
511
|
+
->Array.map(async id =>
|
|
512
|
+
switch (await load(id))->Array.get(0) {
|
|
513
|
+
| Some(item) => (id, Some(item))
|
|
514
|
+
| None =>
|
|
515
|
+
switch Api_Ids.alternateKey(id) {
|
|
516
|
+
| Some(localId) => (localId, (await load(localId))->Array.get(0))
|
|
517
|
+
| None => (id, None)
|
|
518
|
+
}
|
|
473
519
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
520
|
+
)
|
|
521
|
+
->Promise.all
|
|
522
|
+
loaded
|
|
523
|
+
->Array.filterMap(((id, opt)) =>
|
|
524
|
+
opt
|
|
525
|
+
->Option.filter(item => ownerAllows(~ctx, item))
|
|
526
|
+
->Option.flatMap(item => {
|
|
527
|
+
let (retired, state) = retirementOf(item)
|
|
528
|
+
|
|
529
|
+
// A retired row leaves through this door only where the view
|
|
530
|
+
// said it may. Where it did not, the door answers exactly as
|
|
531
|
+
// every other one does — with nothing.
|
|
532
|
+
if retired && !namesRetired {
|
|
533
|
+
None
|
|
534
|
+
} else {
|
|
535
|
+
let label =
|
|
536
|
+
item
|
|
537
|
+
->JSON.Decode.object
|
|
538
|
+
->Option.flatMap(d => d->Dict.get(labelField))
|
|
539
|
+
->Option.flatMap(JSON.Decode.string)
|
|
540
|
+
// A view with no label field resolves to its id, which is
|
|
541
|
+
// what `labelField`'s own fallback already decided.
|
|
542
|
+
->Option.getOr(id)
|
|
543
|
+
// The picture travels with the name, so a retired row that
|
|
544
|
+
// keeps one keeps the other: an archived product reads as
|
|
545
|
+
// itself on the order that bought it.
|
|
546
|
+
let image = switch (imageSourceOf(), item->JSON.Decode.object) {
|
|
547
|
+
| (Some(source), Some(d)) => d->Reventless.RowImage.refFrom(source)
|
|
548
|
+
| _ => None
|
|
488
549
|
}
|
|
550
|
+
Some(
|
|
551
|
+
Dict.fromArray([
|
|
552
|
+
("id", JSON.Encode.string(id)),
|
|
553
|
+
("label", JSON.Encode.string(label)),
|
|
554
|
+
("image", image->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
555
|
+
("retired", JSON.Encode.bool(retired)),
|
|
556
|
+
("retiredState", state->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
557
|
+
])->JSON.Encode.object,
|
|
558
|
+
)
|
|
489
559
|
}
|
|
490
|
-
)
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
->Option.filter(item => ownerAllows(~ctx, item))
|
|
495
|
-
->Option.flatMap(item => {
|
|
496
|
-
let (retired, state) = retirementOf(item)
|
|
497
|
-
// A retired row leaves through this door only where the view
|
|
498
|
-
// said it may. Where it did not, the door answers exactly as
|
|
499
|
-
// every other one does — with nothing.
|
|
500
|
-
if retired && !namesRetired {
|
|
501
|
-
None
|
|
502
|
-
} else {
|
|
503
|
-
let label =
|
|
504
|
-
item
|
|
505
|
-
->JSON.Decode.object
|
|
506
|
-
->Option.flatMap(d => d->Dict.get(labelField))
|
|
507
|
-
->Option.flatMap(JSON.Decode.string)
|
|
508
|
-
// A view with no label field resolves to its id, which is
|
|
509
|
-
// what `labelField`'s own fallback already decided.
|
|
510
|
-
->Option.getOr(id)
|
|
511
|
-
// The picture travels with the name, so a retired row that
|
|
512
|
-
// keeps one keeps the other: an archived product reads as
|
|
513
|
-
// itself on the order that bought it.
|
|
514
|
-
let image =
|
|
515
|
-
switch (imageSourceOf(), item->JSON.Decode.object) {
|
|
516
|
-
| (Some(source), Some(d)) => d->Reventless.RowImage.refFrom(source)
|
|
517
|
-
| _ => None
|
|
518
|
-
}
|
|
519
|
-
Some(
|
|
520
|
-
Dict.fromArray([
|
|
521
|
-
("id", JSON.Encode.string(id)),
|
|
522
|
-
("label", JSON.Encode.string(label)),
|
|
523
|
-
("image", image->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
524
|
-
("retired", JSON.Encode.bool(retired)),
|
|
525
|
-
(
|
|
526
|
-
"retiredState",
|
|
527
|
-
state->Option.mapOr(JSON.Encode.null, JSON.Encode.string),
|
|
528
|
-
),
|
|
529
|
-
])->JSON.Encode.object,
|
|
530
|
-
)
|
|
531
|
-
}
|
|
532
|
-
})
|
|
533
|
-
)
|
|
534
|
-
->JSON.Encode.array
|
|
535
|
-
| None => []->JSON.Encode.array
|
|
536
|
-
}
|
|
560
|
+
})
|
|
561
|
+
)
|
|
562
|
+
->JSON.Encode.array
|
|
563
|
+
| None => []->JSON.Encode.array
|
|
537
564
|
}
|
|
538
565
|
}
|
|
539
|
-
Some((listQueryName ++ "Refs", resolver))
|
|
540
|
-
} else {
|
|
541
|
-
None
|
|
542
566
|
}
|
|
567
|
+
Some((listQueryName ++ "Refs", resolver))
|
|
568
|
+
} else {
|
|
569
|
+
None
|
|
570
|
+
}
|
|
543
571
|
|
|
544
572
|
// -- List query -------------------------------------------------------------
|
|
545
573
|
// Look up the registered state schema (populated alongside queryFieldNamesRegistry)
|
|
@@ -562,7 +590,10 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
562
590
|
}
|
|
563
591
|
}
|
|
564
592
|
|
|
565
|
-
let (listSdl, listResolver): (
|
|
593
|
+
let (listSdl, listResolver): (
|
|
594
|
+
array<string>,
|
|
595
|
+
ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn,
|
|
596
|
+
) = if connectionSpec {
|
|
566
597
|
// Relay Connection spec format
|
|
567
598
|
let filterTypeName = returnTypeName ++ "Filter"
|
|
568
599
|
let orderByTypes = GraphQL_FragmentGenerator.deriveConnectionOrderByType(
|
|
@@ -570,9 +601,10 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
570
601
|
~capability,
|
|
571
602
|
)
|
|
572
603
|
let hasOrderBy = orderByTypes->Array.length > 0
|
|
573
|
-
let typesToRegister =
|
|
574
|
-
|
|
575
|
-
|
|
604
|
+
let typesToRegister =
|
|
605
|
+
[
|
|
606
|
+
GraphQL_FragmentGenerator.deriveConnectionFilterType(~filterTypeName, ~capability),
|
|
607
|
+
]->Array.concat(orderByTypes)
|
|
576
608
|
server.registerTypes(~sdlTypes=typesToRegister)
|
|
577
609
|
let sdl = [
|
|
578
610
|
GraphQL_FragmentGenerator.deriveConnectionQueryField(
|
|
@@ -591,7 +623,11 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
591
623
|
"endCursor": Nullable.null,
|
|
592
624
|
},
|
|
593
625
|
})
|
|
594
|
-
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
626
|
+
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
627
|
+
_root,
|
|
628
|
+
args,
|
|
629
|
+
ctx,
|
|
630
|
+
) => {
|
|
595
631
|
switch await runInterceptor(~ctx, ~args) {
|
|
596
632
|
| Deny(_) => emptyConnection
|
|
597
633
|
| Allow =>
|
|
@@ -653,7 +689,11 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
653
689
|
} else {
|
|
654
690
|
// Legacy AppSync-style format
|
|
655
691
|
let sdl = [` ${listQueryName}(nextToken: String, limit: Int): ${pluralTypeName}!`]
|
|
656
|
-
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
692
|
+
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
693
|
+
_root,
|
|
694
|
+
args,
|
|
695
|
+
ctx,
|
|
696
|
+
) => {
|
|
657
697
|
switch await runInterceptor(~ctx, ~args) {
|
|
658
698
|
| Deny(_) => Obj.magic({"nextToken": Nullable.null, "scannedCount": 0, "items": []})
|
|
659
699
|
| Allow =>
|
|
@@ -663,7 +703,11 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
663
703
|
// the pre-scoping total would tell a caller how many rows they may not see.
|
|
664
704
|
let items =
|
|
665
705
|
all->Array.filter(item => ownerAllows(~ctx, item) && retiredAllows(~ctx, ~args, item))
|
|
666
|
-
Obj.magic({
|
|
706
|
+
Obj.magic({
|
|
707
|
+
"nextToken": Nullable.null,
|
|
708
|
+
"scannedCount": items->Array.length,
|
|
709
|
+
"items": items,
|
|
710
|
+
})
|
|
667
711
|
}
|
|
668
712
|
}
|
|
669
713
|
(sdl, resolver)
|
|
@@ -683,12 +727,21 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
683
727
|
`input ${filterTypeName} {\n prefix: String\n from: String\n to: String\n eq: String\n order: SortOrder\n}`,
|
|
684
728
|
],
|
|
685
729
|
)
|
|
686
|
-
[
|
|
730
|
+
[
|
|
731
|
+
` ${singleQueryName}Items(id: ID!, filter: ${filterTypeName}, first: Int, after: String, last: Int, before: String): ${connectionTypeName}!`,
|
|
732
|
+
]
|
|
687
733
|
| None => []
|
|
688
734
|
}
|
|
689
|
-
let itemsResolvers: array<(
|
|
735
|
+
let itemsResolvers: array<(
|
|
736
|
+
string,
|
|
737
|
+
ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn,
|
|
738
|
+
)> = switch subIdField {
|
|
690
739
|
| Some(sf) =>
|
|
691
|
-
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
740
|
+
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
741
|
+
_root,
|
|
742
|
+
args,
|
|
743
|
+
ctx,
|
|
744
|
+
) => {
|
|
692
745
|
let emptyConn = Obj.magic({
|
|
693
746
|
"edges": [],
|
|
694
747
|
"pageInfo": {
|
|
@@ -704,26 +757,35 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
704
757
|
let argsDict = args->JSON.Decode.object->Option.getOr(Dict.make())
|
|
705
758
|
let id = argsDict->Dict.get("id")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
706
759
|
let filterDict =
|
|
707
|
-
argsDict
|
|
760
|
+
argsDict
|
|
761
|
+
->Dict.get("filter")
|
|
762
|
+
->Option.flatMap(JSON.Decode.object)
|
|
763
|
+
->Option.getOr(Dict.make())
|
|
708
764
|
let filterPrefix = filterDict->Dict.get("prefix")->Option.flatMap(JSON.Decode.string)
|
|
709
|
-
let filterFrom
|
|
710
|
-
let filterTo
|
|
711
|
-
let filterEq
|
|
712
|
-
let orderDesc
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
765
|
+
let filterFrom = filterDict->Dict.get("from")->Option.flatMap(JSON.Decode.string)
|
|
766
|
+
let filterTo = filterDict->Dict.get("to")->Option.flatMap(JSON.Decode.string)
|
|
767
|
+
let filterEq = filterDict->Dict.get("eq")->Option.flatMap(JSON.Decode.string)
|
|
768
|
+
let orderDesc =
|
|
769
|
+
filterDict
|
|
770
|
+
->Dict.get("order")
|
|
771
|
+
->Option.flatMap(JSON.Decode.string)
|
|
772
|
+
->Option.map(o => o == "DESC")
|
|
773
|
+
->Option.getOr(false)
|
|
774
|
+
let first =
|
|
775
|
+
argsDict->Dict.get("first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
|
|
776
|
+
let after = argsDict->Dict.get("after")->Option.flatMap(JSON.Decode.string)
|
|
777
|
+
let last =
|
|
778
|
+
argsDict->Dict.get("last")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
|
|
779
|
+
let before = argsDict->Dict.get("before")->Option.flatMap(JSON.Decode.string)
|
|
780
|
+
let isBackward = last->Option.isSome
|
|
718
781
|
|
|
719
782
|
switch Bus.getQueryDb(name) {
|
|
720
783
|
| None => emptyConn
|
|
721
784
|
| Some(ops) =>
|
|
722
|
-
let loaded =
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
->Effect.runPromise
|
|
785
|
+
let loaded = await ops.loadStream(id)
|
|
786
|
+
->Stream.runCollect
|
|
787
|
+
->Effect.catchAll(_ => Effect.succeed([]))
|
|
788
|
+
->Effect.runPromise
|
|
727
789
|
// Narrowed here, before the cursor window and the sort-key filter, so
|
|
728
790
|
// every page this door emits is a page of rows the caller owns. Doing
|
|
729
791
|
// it after would hand back short pages with valid cursors.
|
|
@@ -737,7 +799,12 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
737
799
|
switch before->Option.map(decodeCursor) {
|
|
738
800
|
| Some(beforeKey) =>
|
|
739
801
|
allItems->Array.filter(item =>
|
|
740
|
-
item
|
|
802
|
+
item
|
|
803
|
+
->JSON.Decode.object
|
|
804
|
+
->Option.flatMap(d => d->Dict.get(sf))
|
|
805
|
+
->Option.flatMap(JSON.Decode.string)
|
|
806
|
+
->Option.map(v => v < beforeKey)
|
|
807
|
+
->Option.getOr(false)
|
|
741
808
|
)
|
|
742
809
|
| None => allItems
|
|
743
810
|
}
|
|
@@ -745,7 +812,12 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
745
812
|
switch after->Option.map(decodeCursor) {
|
|
746
813
|
| Some(afterKey) =>
|
|
747
814
|
allItems->Array.filter(item =>
|
|
748
|
-
item
|
|
815
|
+
item
|
|
816
|
+
->JSON.Decode.object
|
|
817
|
+
->Option.flatMap(d => d->Dict.get(sf))
|
|
818
|
+
->Option.flatMap(JSON.Decode.string)
|
|
819
|
+
->Option.map(v => v > afterKey)
|
|
820
|
+
->Option.getOr(false)
|
|
749
821
|
)
|
|
750
822
|
| None => allItems
|
|
751
823
|
}
|
|
@@ -759,7 +831,11 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
759
831
|
~from=?filterFrom,
|
|
760
832
|
~to_=?filterTo,
|
|
761
833
|
~eq=?filterEq,
|
|
762
|
-
~reverse=if isBackward {
|
|
834
|
+
~reverse=if isBackward {
|
|
835
|
+
!orderDesc
|
|
836
|
+
} else {
|
|
837
|
+
orderDesc
|
|
838
|
+
},
|
|
763
839
|
~offset=0,
|
|
764
840
|
).items
|
|
765
841
|
|
|
@@ -780,13 +856,22 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
780
856
|
}
|
|
781
857
|
|
|
782
858
|
let getSkValue = item =>
|
|
783
|
-
item
|
|
859
|
+
item
|
|
860
|
+
->JSON.Decode.object
|
|
861
|
+
->Option.flatMap(d => d->Dict.get(sf))
|
|
862
|
+
->Option.flatMap(JSON.Decode.string)
|
|
863
|
+
->Option.getOr("")
|
|
784
864
|
|
|
785
|
-
let edges =
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
let
|
|
865
|
+
let edges =
|
|
866
|
+
pageItems->Array.map(item =>
|
|
867
|
+
Obj.magic({"node": item, "cursor": encodeCursor(getSkValue(item))})
|
|
868
|
+
)
|
|
869
|
+
let startCursor =
|
|
870
|
+
pageItems->Array.get(0)->Option.map(item => encodeCursor(getSkValue(item)))
|
|
871
|
+
let endCursor =
|
|
872
|
+
pageItems
|
|
873
|
+
->Array.get(pageItems->Array.length - 1)
|
|
874
|
+
->Option.map(item => encodeCursor(getSkValue(item)))
|
|
790
875
|
|
|
791
876
|
Obj.magic({
|
|
792
877
|
"edges": edges,
|
|
@@ -814,13 +899,14 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
814
899
|
// the same filter, as `GraphQL_FragmentGenerator`: a field emitted here and
|
|
815
900
|
// not there would make the local SDL disagree with the deployed one.
|
|
816
901
|
let doorIndexes = indexes->Array.filter(ic => !Reventless.ReadModel.isDerivedIndex(ic))
|
|
817
|
-
let indexSdlFields =
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
902
|
+
let indexSdlFields =
|
|
903
|
+
doorIndexes->Array.map((ic: Reventless.ReadModel.indexConfig) =>
|
|
904
|
+
GraphQL_FragmentGenerator.deriveIndexQueryField(
|
|
905
|
+
~singleFieldName=singleQueryName,
|
|
906
|
+
~indexConfig=ic,
|
|
907
|
+
~connectionTypeName=returnTypeName ++ "Connection",
|
|
908
|
+
)
|
|
822
909
|
)
|
|
823
|
-
)
|
|
824
910
|
// Backward paging is refused rather than ignored, and refused on both
|
|
825
911
|
// backends rather than on the one that cannot do it. `listAllItemsConnection`
|
|
826
912
|
// already sets the rule for a door whose read cannot walk backwards — "fail
|
|
@@ -853,7 +939,10 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
853
939
|
let indexConnection = (~args, items: array<JSON.t>) => {
|
|
854
940
|
let arg = key => args->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))
|
|
855
941
|
let after =
|
|
856
|
-
arg("after")
|
|
942
|
+
arg("after")
|
|
943
|
+
->Option.flatMap(JSON.Decode.string)
|
|
944
|
+
->Option.flatMap(s => Int.fromString(s))
|
|
945
|
+
->Option.getOr(-1)
|
|
857
946
|
let first =
|
|
858
947
|
arg("first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)->Option.getOr(50)
|
|
859
948
|
let start = after + 1
|
|
@@ -880,60 +969,69 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
880
969
|
),
|
|
881
970
|
])->JSON.Encode.object
|
|
882
971
|
}
|
|
883
|
-
let indexResolvers: array<(
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
972
|
+
let indexResolvers: array<(
|
|
973
|
+
string,
|
|
974
|
+
ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn,
|
|
975
|
+
)> = doorIndexes->Array.map((ic: Reventless.ReadModel.indexConfig) => {
|
|
976
|
+
let resolverName = GraphQL_FragmentGenerator.indexQueryFieldName(
|
|
977
|
+
~singleFieldName=singleQueryName,
|
|
978
|
+
~index=ic.index,
|
|
979
|
+
)
|
|
980
|
+
// The argument the caller passes and the row field this filters on are
|
|
981
|
+
// one and the same — the door reads the index's key, not its name.
|
|
982
|
+
let filterField = GraphQL_FragmentGenerator.indexKeyField(ic)
|
|
983
|
+
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
984
|
+
_root,
|
|
985
|
+
args,
|
|
986
|
+
ctx,
|
|
987
|
+
) => {
|
|
988
|
+
switch await runInterceptor(~ctx, ~args) {
|
|
989
|
+
| Deny(_) => indexConnection(~args, [])
|
|
990
|
+
| Allow =>
|
|
991
|
+
// After the interceptor, matching the AppSync pipeline, where the
|
|
992
|
+
// interceptor leads the chain and the query function's `request`
|
|
993
|
+
// raises this second.
|
|
994
|
+
rejectBackwardPaging(~args)
|
|
995
|
+
let value =
|
|
996
|
+
args
|
|
997
|
+
->JSON.Decode.object
|
|
998
|
+
->Option.flatMap(d => d->Dict.get(filterField))
|
|
999
|
+
->Option.flatMap(JSON.Decode.string)
|
|
1000
|
+
->Option.getOr("")
|
|
1001
|
+
// Applied to whichever arm answers, rather than inside one of them: the
|
|
1002
|
+
// push-down and the scan are two ways to reach the same rows, and a
|
|
1003
|
+
// narrowing that lives in only one is a hole that appears when a
|
|
1004
|
+
// backend gains or loses an index.
|
|
1005
|
+
let scoped = rows =>
|
|
1006
|
+
rows->Array.filter(item => ownerAllows(~ctx, item) && retiredAllows(~ctx, ~args, item))
|
|
1007
|
+
// Prefer the pushed-down equality lookup (SQLite rides the GSI index;
|
|
1008
|
+
// in-memory reuses its lazy snapshot). Fall back to scan+filter only
|
|
1009
|
+
// if no lookup is registered for this QueryDb.
|
|
1010
|
+
switch Bus.getQueryDbIndexLookup(name) {
|
|
1011
|
+
| Some(lookup) => indexConnection(~args, lookup(filterField, value)->scoped)
|
|
1012
|
+
| None =>
|
|
1013
|
+
switch Bus.getQueryDbScan(name) {
|
|
1014
|
+
| Some(scanAll) =>
|
|
1015
|
+
indexConnection(
|
|
1016
|
+
~args,
|
|
1017
|
+
scanAll()
|
|
1018
|
+
->Array.filter(item =>
|
|
1019
|
+
item
|
|
1020
|
+
->JSON.Decode.object
|
|
1021
|
+
->Option.flatMap(d => d->Dict.get(filterField))
|
|
1022
|
+
->Option.flatMap(JSON.Decode.string)
|
|
1023
|
+
->Option.map(v => v == value)
|
|
1024
|
+
->Option.getOr(false)
|
|
928
1025
|
)
|
|
929
|
-
|
|
930
|
-
|
|
1026
|
+
->scoped,
|
|
1027
|
+
)
|
|
1028
|
+
| None => indexConnection(~args, [])
|
|
931
1029
|
}
|
|
932
1030
|
}
|
|
933
1031
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
)
|
|
1032
|
+
}
|
|
1033
|
+
(resolverName, resolver)
|
|
1034
|
+
})
|
|
937
1035
|
|
|
938
1036
|
// -- Cross-table fields: @resolves / @resolvesMany -------------------------
|
|
939
1037
|
// Field resolvers on this view's object type, following a foreign key into
|
|
@@ -1026,8 +1124,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
1026
1124
|
args,
|
|
1027
1125
|
ctx,
|
|
1028
1126
|
) => {
|
|
1029
|
-
let key =
|
|
1030
|
-
root->sourceField(idField)->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
1127
|
+
let key = root->sourceField(idField)->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
1031
1128
|
let rows = if key == "" {
|
|
1032
1129
|
[]
|
|
1033
1130
|
} else {
|
|
@@ -1088,9 +1185,11 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
1088
1185
|
->Array.filterMap(JSON.Decode.string)
|
|
1089
1186
|
// Missing ids drop out rather than becoming nulls, matching BatchGetItem
|
|
1090
1187
|
// and the by-ids door built on it.
|
|
1091
|
-
let rows = await ids
|
|
1188
|
+
let rows = await ids
|
|
1189
|
+
->Array.map(async key =>
|
|
1092
1190
|
(await loadFrom(~target=targetName, key))->Array.get(0)->Option.map(withId(~key, ...))
|
|
1093
|
-
)
|
|
1191
|
+
)
|
|
1192
|
+
->Promise.all
|
|
1094
1193
|
rows
|
|
1095
1194
|
->Array.filterMap(row =>
|
|
1096
1195
|
row->Option.filter(item => targetAllows(~ctx, ~target=targetName, item))
|
|
@@ -1127,5 +1226,4 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
1127
1226
|
resourcesMaker: _ => [],
|
|
1128
1227
|
}
|
|
1129
1228
|
}
|
|
1130
|
-
|
|
1131
1229
|
}
|