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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +13 -13
  3. package/src/BakedManifest.res +5 -6
  4. package/src/EmitCapabilities.res +3 -1
  5. package/src/LocalPlatformRegistry.res +3 -1
  6. package/src/LocalPlatformStart.res +7 -6
  7. package/src/LocalSeedTarget.res +3 -1
  8. package/src/Platform.res +291 -215
  9. package/src/ShellConfig.res +10 -17
  10. package/src/UiHints.res +8 -14
  11. package/src/UiSlots.res +10 -17
  12. package/src/adapter/Api/LocalEvents_Server.res +5 -3
  13. package/src/adapter/Api/LocalGraphQL_Adapter.res +4 -4
  14. package/src/adapter/Auth/LocalAuth.res +11 -15
  15. package/src/adapter/Auth/UserStore.res +5 -7
  16. package/src/adapter/Backend.res +1 -2
  17. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
  18. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
  19. package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
  20. package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
  21. package/src/adapter/Counter/LocalCounterHandler.res +1 -1
  22. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
  23. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
  24. package/src/adapter/DomainGraphQL_Server.res +35 -34
  25. package/src/adapter/DomainMCP_Server.res +45 -41
  26. package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
  27. package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
  28. package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
  29. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
  30. package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
  31. package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
  32. package/src/adapter/LocalBus.res +12 -12
  33. package/src/adapter/LocalCapabilities.res.mjs +1 -1
  34. package/src/adapter/LocalGeocodeResolvers.res +1 -4
  35. package/src/adapter/LocalSideEffectHandler.res +1 -1
  36. package/src/adapter/LocalStateChangeDescriptor.res +2 -1
  37. package/src/adapter/MCP_ServerInstance.res +51 -39
  38. package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
  39. package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
  40. package/src/adapter/PlatformGraphQL_Server.res +9 -5
  41. package/src/adapter/ProjectionCheckpoint.res +8 -1
  42. package/src/adapter/ProjectionPending.res +1 -3
  43. package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
  44. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
  45. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
  46. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
  47. package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
  48. package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
  49. package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
  50. package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
  51. package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
  52. package/src/adapter/SqliteDriver.res +16 -4
  53. package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
  54. package/src/components/Aggregate_Builder.res +6 -1
  55. package/src/components/Counter_Builder.res +1 -5
  56. package/src/components/DcbEventLog_Builder.res +5 -3
  57. package/src/components/ExtensionPoint_Builder.res +13 -15
  58. package/src/components/InboundTranslationSlice_Builder.res +3 -2
  59. package/src/components/OutboundTranslationSlice_Builder.res +3 -2
  60. package/src/components/ReadModel_Builder.res +18 -20
  61. package/src/components/StateViewSlice_Builder.res +3 -2
  62. package/src/components/Task_Builder.res +12 -13
  63. package/src/reset/LocalSeedReset.res +2 -5
  64. package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
  65. package/src/test/Mocks/MockEventLogStorage.res +8 -3
  66. package/src/test/Mocks/MockQueryDbStorage.res +3 -2
  67. package/src/test/TestRunner.res +1 -1
  68. package/tests/BakedManifestFilesTest.res +7 -10
  69. package/tests/LocalEventTapTest.res +26 -21
  70. package/tests/LocalPlatformStartTest.res +33 -28
  71. package/tests/PluginEventDecodeTest.res +3 -3
  72. package/tests/ShellConfigTest.res +30 -21
  73. package/tests/SplitApiFixtures.res +34 -10
  74. package/tests/SplitApiTest.res +92 -62
  75. package/tests/adapter/BackendParityTest.res +180 -130
  76. package/tests/adapter/CollectNEventsTest.res +6 -4
  77. package/tests/adapter/CommandAuthorizationTest.res +47 -41
  78. package/tests/adapter/CommandTopicChannelTest.res +163 -139
  79. package/tests/adapter/CounterHandlerTest.res +95 -77
  80. package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
  81. package/tests/adapter/DcbEventLogStorageTest.res +264 -207
  82. package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
  83. package/tests/adapter/EventCollectorChannelTest.res +153 -124
  84. package/tests/adapter/EventHistoryResolverTest.res +46 -18
  85. package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
  86. package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
  87. package/tests/adapter/EventTapTest.res +37 -20
  88. package/tests/adapter/EventTopicPublisherTest.res +74 -50
  89. package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
  90. package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
  91. package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
  92. package/tests/adapter/HeartbeatRunnerTest.res +99 -81
  93. package/tests/adapter/InboundTranslationMutationTest.res +4 -1
  94. package/tests/adapter/LocalAuthHttpTest.res +79 -85
  95. package/tests/adapter/LocalAuthLoginTest.res +42 -50
  96. package/tests/adapter/LocalAuthTest.res +8 -16
  97. package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
  98. package/tests/adapter/LocalBusBoundedTest.res +187 -117
  99. package/tests/adapter/LocalBusPubSubTest.res +118 -76
  100. package/tests/adapter/LocalDispatchContextTest.res +1 -2
  101. package/tests/adapter/LocalEvents_ServerTest.res +242 -187
  102. package/tests/adapter/PgProjectionCatchupTest.res +44 -10
  103. package/tests/adapter/ProjectionCheckpointTest.res +261 -206
  104. package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
  105. package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
  106. package/tests/adapter/QueryDbListResolverTest.res +225 -208
  107. package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
  108. package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
  109. package/tests/adapter/QueryDbStorageTest.res +163 -117
  110. package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
  111. package/tests/adapter/QueryEngineTest.res +179 -126
  112. package/tests/adapter/ScheduledPublisherTest.res +171 -135
  113. package/tests/adapter/ServedBucketHttpTest.res +65 -59
  114. package/tests/adapter/SortKey_FilterTest.res +19 -3
  115. package/tests/adapter/SqliteDriverTest.res +13 -10
  116. package/tests/adapter/TaskBucketTest.res +94 -80
  117. package/tests/components/aggregate/AggregateTest.res +31 -18
  118. package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
  119. package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
  120. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
  121. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
  122. package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
  123. package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
  124. package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
  125. package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
  126. package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
  127. package/tests/components/commandtopic/CommandTopicTest.res +24 -15
  128. package/tests/components/counter/CounterFixtures.res +6 -8
  129. package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
  130. package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
  131. package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
  132. package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
  133. package/tests/components/dcb/DcbFixtures.res +4 -1
  134. package/tests/components/dcb/DcbTest.res +9 -6
  135. package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
  136. package/tests/components/eventcollector/EventCollectorTest.res +6 -3
  137. package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
  138. package/tests/components/eventlog/EventLogConflictTest.res +8 -4
  139. package/tests/components/eventlog/EventLogFixtures.res +3 -3
  140. package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
  141. package/tests/components/eventlog/EventLogStreamTest.res +119 -56
  142. package/tests/components/eventlog/EventLogTest.res +20 -4
  143. package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
  144. package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
  145. package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
  146. package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
  147. package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
  148. package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
  149. package/tests/components/heartbeat/HeartbeatTest.res +56 -47
  150. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
  151. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
  152. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
  153. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
  154. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
  155. package/tests/components/querydb/QueryDbFixtures.res +1 -5
  156. package/tests/components/querydb/QueryDbTest.res +2 -4
  157. package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
  158. package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
  159. package/tests/components/readmodel/ReadModelFixtures.res +8 -8
  160. package/tests/components/scheduler/SchedulerTest.res +76 -58
  161. package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
  162. package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
  163. package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
  164. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
  165. package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
  166. package/tests/components/task/TaskFixtures.res +5 -5
  167. package/tests/components/task/TaskTest.res +95 -80
  168. package/tests/plugin/PluginsProjection_GWT.res +3 -1
  169. package/tests/plugin/UiFragments_GWT.res +6 -1
  170. package/tests/reset/LocalSeedResetTest.res +1 -0
@@ -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(DomainGraphQL_Server.asInterface)
61
- let relayRef: ref<option<relaySupport>> = ref(Some({
62
- encodeGlobalId: DomainGraphQL_Server.encodeGlobalId,
63
- registerNodeType: DomainGraphQL_Server.registerNodeType,
64
- registerNodeResolverCallback: DomainGraphQL_Server.registerNodeResolverCallback,
65
- nodeTypeRegistry: DomainGraphQL_Server.nodeTypeRegistry,
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
- await ops.loadStream(localId)
106
- ->Stream.runCollect
107
- ->Effect.catchAll(_ => Effect.succeed([]))
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 (_root, args, ctx) => {
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->JSON.Decode.object->Option.flatMap(d => d->Dict.get("id"))->Option.flatMap(JSON.Decode.string)->Option.getOr("")
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) if !ownerAllows(~ctx, item) || !retiredAllows(~ctx, ~args, 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
- [GraphQL_FragmentGenerator.deriveByIdsQueryField(~listFieldName=listQueryName, ~returnTypeName)]
361
+ [
362
+ GraphQL_FragmentGenerator.deriveByIdsQueryField(
363
+ ~listFieldName=listQueryName,
364
+ ~returnTypeName,
365
+ ),
366
+ ]
353
367
  } else {
354
368
  []
355
369
  }
356
- let byIdsResolverEntry: option<(string, ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn)> =
357
- if includeIdParam && subIdField === None {
358
- let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (_root, args, ctx) => {
359
- switch await runInterceptor(~ctx, ~args) {
360
- | Deny(_) => []->JSON.Encode.array
361
- | Allow =>
362
- let ids =
363
- args
364
- ->JSON.Decode.object
365
- ->Option.flatMap(d => d->Dict.get("ids"))
366
- ->Option.flatMap(JSON.Decode.array)
367
- ->Option.getOr([])
368
- ->Array.filterMap(JSON.Decode.string)
369
- switch Bus.getQueryDb(name) {
370
- | Some(ops) =>
371
- let load = key =>
372
- ops.loadStream(key)
373
- ->Stream.runCollect
374
- ->Effect.catchAll(_ => Effect.succeed([]))
375
- ->Effect.runPromise
376
- // Same either-form rule as the single-id door: raw key first, the
377
- // key inside a Relay global id only on a miss.
378
- let loaded = await ids->Array.map(async id =>
379
- switch (await load(id))->Array.get(0) {
380
- | Some(item) => (id, Some(item))
381
- | None =>
382
- switch Api_Ids.alternateKey(id) {
383
- | Some(localId) => (localId, (await load(localId))->Array.get(0))
384
- | None => (id, None)
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
- )->Promise.all
388
- loaded
389
- ->Array.filterMap(((id, opt)) =>
390
- opt
391
- ->Option.filter(item => ownerAllows(~ctx, item) && retiredAllows(~ctx, ~args, item))
392
- ->Option.map(item => {
393
- let obj = item->JSON.Decode.object->Option.mapOr(Dict.make(), Dict.copy)
394
- obj->Dict.set("id", JSON.Encode.string(id))
395
- JSON.Encode.object(obj)
396
- })
397
- )
398
- ->JSON.Encode.array
399
- | None => []->JSON.Encode.array
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
- [GraphQL_FragmentGenerator.deriveRefsQueryField(~listFieldName=listQueryName, ~returnTypeName)]
451
+ [
452
+ GraphQL_FragmentGenerator.deriveRefsQueryField(
453
+ ~listFieldName=listQueryName,
454
+ ~returnTypeName,
455
+ ),
456
+ ]
430
457
  } else {
431
458
  []
432
459
  }
433
- let refsResolverEntry: option<(string, ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn)> =
434
- if includeIdParam && subIdField === None {
435
- server.registerTypes(~sdlTypes=[GraphQL_FragmentGenerator.deriveRefTypeSdl(~returnTypeName)])
436
- let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
437
- _root,
438
- args,
439
- ctx,
440
- ) => {
441
- switch await runInterceptor(~ctx, ~args) {
442
- | Deny(_) => []->JSON.Encode.array
443
- | Allow =>
444
- let ids =
445
- args
446
- ->JSON.Decode.object
447
- ->Option.flatMap(d => d->Dict.get("ids"))
448
- ->Option.flatMap(JSON.Decode.array)
449
- ->Option.getOr([])
450
- ->Array.filterMap(JSON.Decode.string)
451
- let spec = retiredSpecOf()
452
- let namesRetired = spec->Option.mapOr(false, r => r.namedWhenRetired)
453
- // The row's own retirement, read with the same `isRetiredValue` every
454
- // other door narrows by so "retired" here and "withheld" there are
455
- // one answer, not two that drift.
456
- let retirementOf = (item: JSON.t) =>
457
- switch spec {
458
- | None => (false, None)
459
- | Some(r) =>
460
- let cell = item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(r.field))
461
- let scope: Reventless.OwnerScope.retiredScope = {field: r.field, values: r.values}
462
- let retired = scope->Reventless.OwnerScope.isRetiredValue(cell)
463
- (
464
- retired,
465
- // The state that retired it, and only that. A live row reports
466
- // none — this door names rows, it does not publish a lifecycle
467
- // column to callers the list withholds. Nor does the boolean
468
- // form, where the field is the state and `retired` said it.
469
- retired && r.values->Option.isSome
470
- ? cell->Option.flatMap(JSON.Decode.string)
471
- : None,
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
- switch Bus.getQueryDb(name) {
475
- | Some(ops) =>
476
- let load = key =>
477
- ops.loadStream(key)
478
- ->Stream.runCollect
479
- ->Effect.catchAll(_ => Effect.succeed([]))
480
- ->Effect.runPromise
481
- let loaded = await ids->Array.map(async id =>
482
- switch (await load(id))->Array.get(0) {
483
- | Some(item) => (id, Some(item))
484
- | None =>
485
- switch Api_Ids.alternateKey(id) {
486
- | Some(localId) => (localId, (await load(localId))->Array.get(0))
487
- | None => (id, None)
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
- )->Promise.all
491
- loaded
492
- ->Array.filterMap(((id, opt)) =>
493
- opt
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): (array<string>, ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn) = if connectionSpec {
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
- GraphQL_FragmentGenerator.deriveConnectionFilterType(~filterTypeName, ~capability),
575
- ]->Array.concat(orderByTypes)
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 (_root, args, ctx) => {
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 (_root, args, ctx) => {
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({"nextToken": Nullable.null, "scannedCount": items->Array.length, "items": items})
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
- [` ${singleQueryName}Items(id: ID!, filter: ${filterTypeName}, first: Int, after: String, last: Int, before: String): ${connectionTypeName}!`]
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<(string, ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn)> = switch subIdField {
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 (_root, args, ctx) => {
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->Dict.get("filter")->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
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 = filterDict->Dict.get("from")->Option.flatMap(JSON.Decode.string)
710
- let filterTo = filterDict->Dict.get("to")->Option.flatMap(JSON.Decode.string)
711
- let filterEq = filterDict->Dict.get("eq")->Option.flatMap(JSON.Decode.string)
712
- let orderDesc = filterDict->Dict.get("order")->Option.flatMap(JSON.Decode.string)->Option.map(o => o == "DESC")->Option.getOr(false)
713
- let first = argsDict->Dict.get("first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
714
- let after = argsDict->Dict.get("after")->Option.flatMap(JSON.Decode.string)
715
- let last = argsDict->Dict.get("last")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
716
- let before = argsDict->Dict.get("before")->Option.flatMap(JSON.Decode.string)
717
- let isBackward = last->Option.isSome
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
- await ops.loadStream(id)
724
- ->Stream.runCollect
725
- ->Effect.catchAll(_ => Effect.succeed([]))
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->JSON.Decode.object->Option.flatMap(d => d->Dict.get(sf))->Option.flatMap(JSON.Decode.string)->Option.map(v => v < beforeKey)->Option.getOr(false)
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->JSON.Decode.object->Option.flatMap(d => d->Dict.get(sf))->Option.flatMap(JSON.Decode.string)->Option.map(v => v > afterKey)->Option.getOr(false)
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 { !orderDesc } else { orderDesc },
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->JSON.Decode.object->Option.flatMap(d => d->Dict.get(sf))->Option.flatMap(JSON.Decode.string)->Option.getOr("")
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 = pageItems->Array.map(item =>
786
- Obj.magic({"node": item, "cursor": encodeCursor(getSkValue(item))})
787
- )
788
- let startCursor = pageItems->Array.get(0)->Option.map(item => encodeCursor(getSkValue(item)))
789
- let endCursor = pageItems->Array.get(pageItems->Array.length - 1)->Option.map(item => encodeCursor(getSkValue(item)))
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 = doorIndexes->Array.map((ic: Reventless.ReadModel.indexConfig) =>
818
- GraphQL_FragmentGenerator.deriveIndexQueryField(
819
- ~singleFieldName=singleQueryName,
820
- ~indexConfig=ic,
821
- ~connectionTypeName=returnTypeName ++ "Connection",
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")->Option.flatMap(JSON.Decode.string)->Option.flatMap(s => Int.fromString(s))->Option.getOr(-1)
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<(string, ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn)> = doorIndexes->Array.map(
884
- (ic: Reventless.ReadModel.indexConfig) => {
885
- let resolverName = GraphQL_FragmentGenerator.indexQueryFieldName(
886
- ~singleFieldName=singleQueryName,
887
- ~index=ic.index,
888
- )
889
- // The argument the caller passes and the row field this filters on are
890
- // one and the same — the door reads the index's key, not its name.
891
- let filterField = GraphQL_FragmentGenerator.indexKeyField(ic)
892
- let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (_root, args, ctx) => {
893
- switch await runInterceptor(~ctx, ~args) {
894
- | Deny(_) => indexConnection(~args, [])
895
- | Allow =>
896
- // After the interceptor, matching the AppSync pipeline, where the
897
- // interceptor leads the chain and the query function's `request`
898
- // raises this second.
899
- rejectBackwardPaging(~args)
900
- let value =
901
- args->JSON.Decode.object->Option.flatMap(d => d->Dict.get(filterField))->Option.flatMap(JSON.Decode.string)->Option.getOr("")
902
- // Applied to whichever arm answers, rather than inside one of them: the
903
- // push-down and the scan are two ways to reach the same rows, and a
904
- // narrowing that lives in only one is a hole that appears when a
905
- // backend gains or loses an index.
906
- let scoped = rows =>
907
- rows->Array.filter(item => ownerAllows(~ctx, item) && retiredAllows(~ctx, ~args, item))
908
- // Prefer the pushed-down equality lookup (SQLite rides the GSI index;
909
- // in-memory reuses its lazy snapshot). Fall back to scan+filter only
910
- // if no lookup is registered for this QueryDb.
911
- switch Bus.getQueryDbIndexLookup(name) {
912
- | Some(lookup) => indexConnection(~args, lookup(filterField, value)->scoped)
913
- | None =>
914
- switch Bus.getQueryDbScan(name) {
915
- | Some(scanAll) =>
916
- indexConnection(
917
- ~args,
918
- scanAll()
919
- ->Array.filter(item =>
920
- item
921
- ->JSON.Decode.object
922
- ->Option.flatMap(d => d->Dict.get(filterField))
923
- ->Option.flatMap(JSON.Decode.string)
924
- ->Option.map(v => v == value)
925
- ->Option.getOr(false)
926
- )
927
- ->scoped,
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
- | None => indexConnection(~args, [])
930
- }
1026
+ ->scoped,
1027
+ )
1028
+ | None => indexConnection(~args, [])
931
1029
  }
932
1030
  }
933
1031
  }
934
- (resolverName, resolver)
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->Array.map(async key =>
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
- )->Promise.all
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
  }