@reventlessdev/reventless-local 3.0.0-alpha.226 → 3.0.0-alpha.228

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 (84) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +14 -14
  3. package/src/LocalPlatformRegistry.res +2 -2
  4. package/src/LocalPlatformRegistry.res.mjs +14 -13
  5. package/src/Platform.res +14 -15
  6. package/src/Platform.res.mjs +20 -21
  7. package/src/adapter/Auth/LocalAuth.res +3 -3
  8. package/src/adapter/Auth/LocalAuth.res.mjs +3 -2
  9. package/src/adapter/Auth/UserStore.res +1 -1
  10. package/src/adapter/Auth/UserStore.res.mjs +7 -6
  11. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +1 -1
  12. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +1 -1
  13. package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +1 -1
  14. package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res.mjs +3 -3
  15. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +2 -2
  16. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +4 -4
  17. package/src/adapter/DomainGraphQL_Server.res +2 -2
  18. package/src/adapter/DomainGraphQL_Server.res.mjs +3 -3
  19. package/src/adapter/DomainMCP_Server.res +1 -1
  20. package/src/adapter/MCP_ServerInstance.res +1 -1
  21. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +223 -22
  22. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +176 -18
  23. package/tests/BakedManifestFilesTest.res +0 -1
  24. package/tests/PluginEventDecodeTest.res +3 -3
  25. package/tests/PluginEventDecodeTest.res.mjs +4 -4
  26. package/tests/ShellConfigTest.res +0 -1
  27. package/tests/UiHintsTest.res +33 -6
  28. package/tests/UiHintsTest.res.mjs +15 -2
  29. package/tests/adapter/CommandAuthorizationTest.res.mjs +7 -7
  30. package/tests/adapter/GraphQL_SchemaInspectorTest.res +9 -7
  31. package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +64 -64
  32. package/tests/adapter/LocalAuthHttpTest.res +1 -1
  33. package/tests/adapter/LocalAuthHttpTest.res.mjs +2 -2
  34. package/tests/adapter/QueryDbListResolverTest.res +82 -2
  35. package/tests/adapter/QueryDbListResolverTest.res.mjs +111 -16
  36. package/tests/components/aggregate/AggregateFixtures.res.mjs +6 -6
  37. package/tests/components/automationslice/AutomationSliceFixtures.res +4 -4
  38. package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +20 -19
  39. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +2 -2
  40. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +23 -22
  41. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +4 -4
  42. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +21 -20
  43. package/tests/components/commandgenerator/CommandGeneratorFixtures.res +0 -1
  44. package/tests/components/commandgenerator/CommandGeneratorFixtures.res.mjs +6 -8
  45. package/tests/components/commandgenerator/CommandGeneratorTest.res +1 -1
  46. package/tests/components/commandgenerator/CommandGeneratorTest.res.mjs +2 -2
  47. package/tests/components/commandtopic/CommandTopicFixtures.res.mjs +9 -9
  48. package/tests/components/commandtopic/CommandTopicStreamFixtures.res.mjs +9 -9
  49. package/tests/components/dcb/DcbCrossPartitionFixtures.res +3 -3
  50. package/tests/components/dcb/DcbCrossPartitionFixtures.res.mjs +22 -21
  51. package/tests/components/dcb/DcbEventLogAppendStreamTest.res +1 -1
  52. package/tests/components/dcb/DcbEventLogAppendStreamTest.res.mjs +2 -2
  53. package/tests/components/dcb/DcbFixtures.res +2 -2
  54. package/tests/components/dcb/DcbFixtures.res.mjs +12 -11
  55. package/tests/components/eventlog/EventLogFixtures.res.mjs +6 -6
  56. package/tests/components/eventlog/EventLogProvisioningSeamTest.res.mjs +3 -3
  57. package/tests/components/eventtopic/EventTopicFixtures.res.mjs +6 -6
  58. package/tests/components/eventtopic/EventTopicStreamFixtures.res.mjs +6 -6
  59. package/tests/components/extensionpoint/ExtensionPointFixtures.res +0 -1
  60. package/tests/components/extensionpoint/ExtensionPointFixtures.res.mjs +11 -13
  61. package/tests/components/extensionpoint/ExtensionPointTest.res +3 -3
  62. package/tests/components/extensionpoint/ExtensionPointTest.res.mjs +4 -4
  63. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs +10 -10
  64. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res.mjs +9 -9
  65. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res.mjs +23 -21
  66. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +2 -2
  67. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs +17 -14
  68. package/tests/components/querydb/QueryDbFixtures.res.mjs +4 -4
  69. package/tests/components/querydb/QueryDbSubIdFixtures.res.mjs +6 -6
  70. package/tests/components/readmodel/DcbReadModelE2EFixtures.res +3 -3
  71. package/tests/components/readmodel/DcbReadModelE2EFixtures.res.mjs +17 -16
  72. package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res.mjs +7 -7
  73. package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -5
  74. package/tests/components/readmodel/MixedSourceReadModelFixtures.res.mjs +20 -19
  75. package/tests/components/readmodel/ReadModelFixtures.res +3 -3
  76. package/tests/components/readmodel/ReadModelFixtures.res.mjs +9 -8
  77. package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +2 -3
  78. package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res.mjs +7 -8
  79. package/tests/components/stateviewslice/StateViewSliceFixtures.res +2 -2
  80. package/tests/components/stateviewslice/StateViewSliceFixtures.res.mjs +22 -21
  81. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +2 -2
  82. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res.mjs +27 -26
  83. package/tests/components/task/TaskFixtures.res +0 -1
  84. package/tests/components/task/TaskFixtures.res.mjs +0 -3
@@ -110,7 +110,7 @@ let handleLogin = (req: nodeRequest, res: nodeResponse): unit =>
110
110
  | None => Reventless.Identity.anonymous
111
111
  }
112
112
  let identityJson =
113
- identity->S.reverseConvertToJsonOrThrow(Reventless.Identity.schema)
113
+ identity->Reventless.Util_Sury.toJson(Reventless.Identity.schema)
114
114
  _writeJson(
115
115
  res,
116
116
  ~status=200,
@@ -171,7 +171,7 @@ let handleSwitchRole = (req: nodeRequest, res: nodeResponse): unit =>
171
171
  ("token", JSON.Encode.string(newToken)),
172
172
  (
173
173
  "identity",
174
- identity->S.reverseConvertToJsonOrThrow(Reventless.Identity.schema),
174
+ identity->Reventless.Util_Sury.toJson(Reventless.Identity.schema),
175
175
  ),
176
176
  ]),
177
177
  ),
@@ -1,6 +1,5 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as S from "sury/src/S.res.mjs";
4
3
  import * as Http from "http";
5
4
  import * as Graphql from "graphql";
6
5
  import * as GraphqlYoga from "@reventlessdev/rescript-graphql-yoga/src/GraphqlYoga.res.mjs";
@@ -14,6 +13,7 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
14
13
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
15
14
  import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
16
15
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
16
+ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
17
17
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
18
18
  import * as Api_Ids$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Ids.res.mjs";
19
19
  import * as LocalAuth$ReventlessLocal from "./Auth/LocalAuth.res.mjs";
@@ -75,7 +75,7 @@ function handleLogin(req, res) {
75
75
  if (result.TAG === "Ok") {
76
76
  let i = LocalAuth$ReventlessLocal.Login.mintedIdentity(username, activeRole);
77
77
  let identity = i !== undefined ? i : Identity$Reventless.anonymous;
78
- let identityJson = S.reverseConvertToJsonOrThrow(identity, Identity$Reventless.schema);
78
+ let identityJson = Util_Sury$Reventless.toJson(identity, Identity$Reventless.schema);
79
79
  _writeJson(res, 200, Object.fromEntries([
80
80
  [
81
81
  "token",
@@ -130,7 +130,7 @@ function handleSwitchRole(req, res) {
130
130
  ],
131
131
  [
132
132
  "identity",
133
- S.reverseConvertToJsonOrThrow(identity, Identity$Reventless.schema)
133
+ Util_Sury$Reventless.toJson(identity, Identity$Reventless.schema)
134
134
  ]
135
135
  ]));
136
136
  });
@@ -143,7 +143,7 @@ let extractIdentity = (req: McpSdk.incomingMessage): Reventless.Identity.t => {
143
143
  | Some(json) =>
144
144
  json
145
145
  ->JSON.parseOrThrow
146
- ->S.parseOrThrow(Reventless.Identity.schema)
146
+ ->S.parseOrThrow(~to=Reventless.Identity.schema)
147
147
  | None => Reventless.Identity.anonymous
148
148
  }
149
149
  } catch {
@@ -157,7 +157,7 @@ let make = (~label: string="MCP"): t => {
157
157
  | Some(json) =>
158
158
  json
159
159
  ->JSON.parseOrThrow
160
- ->S.parseOrThrow(Reventless.Identity.schema)
160
+ ->S.parseOrThrow(~to=Reventless.Identity.schema)
161
161
  | None => Reventless.Identity.anonymous
162
162
  }
163
163
  } catch {
@@ -228,8 +228,6 @@ module Make = (Bus: LocalBus.T) => {
228
228
  )
229
229
  }
230
230
 
231
- let cap = s => s->String.charAt(0)->String.toUpperCase ++ s->String.slice(~start=1)
232
-
233
231
  // Resolve query field names: check registry first, fall back to safe defaults.
234
232
  // Fallbacks use simple GraphQL built-in types to avoid referencing non-existent custom types.
235
233
  let registryEntry = Plugin_Helpers.queryFieldNamesRegistry->Dict.get(name)
@@ -271,10 +269,16 @@ module Make = (Bus: LocalBus.T) => {
271
269
  }
272
270
 
273
271
  // -- Main query: getById ---------------------------------------------------
272
+ // `includeRetired` here matches the connection field and the AppSync SDL the
273
+ // FragmentGenerator emits. The resolver below already reads it — `retiredAllows`
274
+ // consults `askedForRetired(~args)` on every door — so this argument was the
275
+ // only missing half, and without it an elevated caller could see an archived
276
+ // row in a list and not open it.
274
277
  let byIdSdl = if includeIdParam {
275
278
  switch subIdField {
276
- | Some(sf) => ` ${singleQueryName}(id: ID!, ${sf}: String): ${returnTypeName}`
277
- | None => ` ${singleQueryName}(id: ID!): ${returnTypeName}`
279
+ | Some(sf) =>
280
+ ` ${singleQueryName}(id: ID!, ${sf}: String, includeRetired: Boolean): ${returnTypeName}`
281
+ | None => ` ${singleQueryName}(id: ID!, includeRetired: Boolean): ${returnTypeName}`
278
282
  }
279
283
  } else {
280
284
  ` ${singleQueryName}: ${returnTypeName}`
@@ -402,6 +406,124 @@ module Make = (Bus: LocalBus.T) => {
402
406
  | None => "id"
403
407
  }
404
408
 
409
+ // -- Reference door: {listFieldName}Refs -----------------------------------
410
+ // Names the rows a caller already holds pointers to, and nothing more. Three
411
+ // fields, fixed by the type, so this cannot be widened by what a caller
412
+ // selects.
413
+ //
414
+ // Retirement is lifted here only where the view declared `@namedWhenRetired`.
415
+ // Ownership is not lifted at all, and the asymmetry is the point: an archived
416
+ // product is a thing the shop sold and every order naming one may say what it
417
+ // was, while a deactivated customer is somebody's row before it is anything
418
+ // else. One predicate answers "is this row still on offer", the other "is this
419
+ // row yours", and only the first is what an archive withdraws.
420
+ let refsSdl = if includeIdParam && subIdField === None {
421
+ [GraphQL_FragmentGenerator.deriveRefsQueryField(~listFieldName=listQueryName, ~returnTypeName)]
422
+ } else {
423
+ []
424
+ }
425
+ let refsResolverEntry: option<(string, ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn)> =
426
+ if includeIdParam && subIdField === None {
427
+ server.registerTypes(~sdlTypes=[GraphQL_FragmentGenerator.deriveRefTypeSdl(~returnTypeName)])
428
+ let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
429
+ _root,
430
+ args,
431
+ ctx,
432
+ ) => {
433
+ switch await runInterceptor(~ctx, ~args) {
434
+ | Deny(_) => []->JSON.Encode.array
435
+ | Allow =>
436
+ let ids =
437
+ args
438
+ ->JSON.Decode.object
439
+ ->Option.flatMap(d => d->Dict.get("ids"))
440
+ ->Option.flatMap(JSON.Decode.array)
441
+ ->Option.getOr([])
442
+ ->Array.filterMap(JSON.Decode.string)
443
+ let spec = retiredSpecOf()
444
+ let namesRetired = spec->Option.mapOr(false, r => r.namedWhenRetired)
445
+ // The row's own retirement, read with the same `isRetiredValue` every
446
+ // other door narrows by — so "retired" here and "withheld" there are
447
+ // one answer, not two that drift.
448
+ let retirementOf = (item: JSON.t) =>
449
+ switch spec {
450
+ | None => (false, None)
451
+ | Some(r) =>
452
+ let cell = item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(r.field))
453
+ let scope: Reventless.OwnerScope.retiredScope = {field: r.field, values: r.values}
454
+ let retired = scope->Reventless.OwnerScope.isRetiredValue(cell)
455
+ (
456
+ retired,
457
+ // The state that retired it, and only that. A live row reports
458
+ // none — this door names rows, it does not publish a lifecycle
459
+ // column to callers the list withholds. Nor does the boolean
460
+ // form, where the field is the state and `retired` said it.
461
+ retired && r.values->Option.isSome
462
+ ? cell->Option.flatMap(JSON.Decode.string)
463
+ : None,
464
+ )
465
+ }
466
+ switch Bus.getQueryDb(name) {
467
+ | Some(ops) =>
468
+ let load = key =>
469
+ ops.loadStream(key)
470
+ ->Stream.runCollect
471
+ ->Effect.catchAll(_ => Effect.succeed([]))
472
+ ->Effect.runPromise
473
+ let loaded = await ids->Array.map(async id =>
474
+ switch (await load(id))->Array.get(0) {
475
+ | Some(item) => (id, Some(item))
476
+ | None =>
477
+ switch Api_Ids.alternateKey(id) {
478
+ | Some(localId) => (localId, (await load(localId))->Array.get(0))
479
+ | None => (id, None)
480
+ }
481
+ }
482
+ )->Promise.all
483
+ loaded
484
+ ->Array.filterMap(((id, opt)) =>
485
+ opt
486
+ ->Option.filter(item => ownerAllows(~ctx, item))
487
+ ->Option.flatMap(item => {
488
+ let (retired, state) = retirementOf(item)
489
+ // A retired row leaves through this door only where the view
490
+ // said it may. Where it did not, the door answers exactly as
491
+ // every other one does — with nothing.
492
+ if retired && !namesRetired {
493
+ None
494
+ } else {
495
+ let label =
496
+ item
497
+ ->JSON.Decode.object
498
+ ->Option.flatMap(d => d->Dict.get(labelField))
499
+ ->Option.flatMap(JSON.Decode.string)
500
+ // A view with no label field resolves to its id, which is
501
+ // what `labelField`'s own fallback already decided.
502
+ ->Option.getOr(id)
503
+ Some(
504
+ Dict.fromArray([
505
+ ("id", JSON.Encode.string(id)),
506
+ ("label", JSON.Encode.string(label)),
507
+ ("retired", JSON.Encode.bool(retired)),
508
+ (
509
+ "retiredState",
510
+ state->Option.mapOr(JSON.Encode.null, JSON.Encode.string),
511
+ ),
512
+ ])->JSON.Encode.object,
513
+ )
514
+ }
515
+ })
516
+ )
517
+ ->JSON.Encode.array
518
+ | None => []->JSON.Encode.array
519
+ }
520
+ }
521
+ }
522
+ Some((listQueryName ++ "Refs", resolver))
523
+ } else {
524
+ None
525
+ }
526
+
405
527
  // -- List query -------------------------------------------------------------
406
528
  // Look up the registered state schema (populated alongside queryFieldNamesRegistry)
407
529
  // so the resolver derives the same serverCapability the FragmentGenerator emitted.
@@ -666,20 +788,95 @@ module Make = (Bus: LocalBus.T) => {
666
788
  }
667
789
 
668
790
  // -- Index queries: {name}By{Index} ---------------------------------------
791
+ // Name, argument and return type all come from `GraphQL_FragmentGenerator`,
792
+ // which is also where the AppSync SDL gets them. They used to be spelled out
793
+ // here independently and disagreed with that emitter on all three, so the
794
+ // same door had a different signature per backend and worked on neither.
669
795
  let indexSdlFields = indexes->Array.map((ic: Reventless.ReadModel.indexConfig) =>
670
- ` ${singleQueryName}By${cap(ic.index)}(${ic.index}: String!): [String]`
796
+ GraphQL_FragmentGenerator.deriveIndexQueryField(
797
+ ~singleFieldName=singleQueryName,
798
+ ~indexConfig=ic,
799
+ ~connectionTypeName=returnTypeName ++ "Connection",
800
+ )
671
801
  )
802
+ // Backward paging is refused rather than ignored, and refused on both
803
+ // backends rather than on the one that cannot do it. `listAllItemsConnection`
804
+ // already sets the rule for a door whose read cannot walk backwards — "fail
805
+ // loud rather than silently returning the forward page" — and its reasoning
806
+ // is about the answer, not about DynamoDB: `last: 2` handed the first two
807
+ // rows is a different question answered without saying so.
808
+ //
809
+ // The arguments stay in the SDL. One that appeared and disappeared with the
810
+ // backend would make adding a sort key a breaking schema change and force
811
+ // every client to feature-detect, which is the same argument `includeRetired`
812
+ // and the reference door are emitted unconditionally for.
813
+ let rejectBackwardPaging = (~args) => {
814
+ let given = key =>
815
+ switch args->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key)) {
816
+ | None | Some(JSON.Null) => false
817
+ | Some(_) => true
818
+ }
819
+ if given("last") || given("before") {
820
+ throw(
821
+ GraphQL_CallerError.badUserInput(
822
+ "Backward pagination (last/before) is not supported on by-index connections; use first/after.",
823
+ ),
824
+ )
825
+ }
826
+ }
827
+ // A positional connection. This door answers an equality lookup on one index
828
+ // value and has no ordering key to cut a cursor from — the list door borrows
829
+ // its sort key because it orders by one. An offset is stable for as long as
830
+ // a page-forward takes, which is what the field advertises and no more.
831
+ let indexConnection = (~args, items: array<JSON.t>) => {
832
+ let arg = key => args->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))
833
+ let after =
834
+ arg("after")->Option.flatMap(JSON.Decode.string)->Option.flatMap(s => Int.fromString(s))->Option.getOr(-1)
835
+ let first =
836
+ arg("first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)->Option.getOr(50)
837
+ let start = after + 1
838
+ let page = items->Array.slice(~start, ~end=start + first)
839
+ let taken = page->Array.length
840
+ let edges =
841
+ page->Array.mapWithIndex((item, i) =>
842
+ Dict.fromArray([
843
+ ("node", item),
844
+ ("cursor", (start + i)->Int.toString->JSON.Encode.string),
845
+ ])->JSON.Encode.object
846
+ )
847
+ let cursorAt = i => i->Int.toString->JSON.Encode.string
848
+ Dict.fromArray([
849
+ ("edges", edges->JSON.Encode.array),
850
+ (
851
+ "pageInfo",
852
+ Dict.fromArray([
853
+ ("hasNextPage", JSON.Encode.bool(start + taken < items->Array.length)),
854
+ ("hasPreviousPage", JSON.Encode.bool(start > 0)),
855
+ ("startCursor", taken > 0 ? cursorAt(start) : JSON.Encode.null),
856
+ ("endCursor", taken > 0 ? cursorAt(start + taken - 1) : JSON.Encode.null),
857
+ ])->JSON.Encode.object,
858
+ ),
859
+ ])->JSON.Encode.object
860
+ }
672
861
  let indexResolvers: array<(string, ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn)> = indexes->Array.map(
673
862
  (ic: Reventless.ReadModel.indexConfig) => {
674
- let index = ic.index
675
- let resolverName = singleQueryName ++ "By" ++ cap(index)
676
- let filterField = ic.idField->Option.getOr(index)
863
+ let resolverName = GraphQL_FragmentGenerator.indexQueryFieldName(
864
+ ~singleFieldName=singleQueryName,
865
+ ~index=ic.index,
866
+ )
867
+ // The argument the caller passes and the row field this filters on are
868
+ // one and the same — the door reads the index's key, not its name.
869
+ let filterField = GraphQL_FragmentGenerator.indexKeyField(ic)
677
870
  let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (_root, args, ctx) => {
678
871
  switch await runInterceptor(~ctx, ~args) {
679
- | Deny(_) => []->JSON.Encode.array
872
+ | Deny(_) => indexConnection(~args, [])
680
873
  | Allow =>
874
+ // After the interceptor, matching the AppSync pipeline, where the
875
+ // interceptor leads the chain and the query function's `request`
876
+ // raises this second.
877
+ rejectBackwardPaging(~args)
681
878
  let value =
682
- args->JSON.Decode.object->Option.flatMap(d => d->Dict.get(index))->Option.flatMap(JSON.Decode.string)->Option.getOr("")
879
+ args->JSON.Decode.object->Option.flatMap(d => d->Dict.get(filterField))->Option.flatMap(JSON.Decode.string)->Option.getOr("")
683
880
  // Applied to whichever arm answers, rather than inside one of them: the
684
881
  // push-down and the scan are two ways to reach the same rows, and a
685
882
  // narrowing that lives in only one is a hole that appears when a
@@ -690,22 +887,24 @@ module Make = (Bus: LocalBus.T) => {
690
887
  // in-memory reuses its lazy snapshot). Fall back to scan+filter only
691
888
  // if no lookup is registered for this QueryDb.
692
889
  switch Bus.getQueryDbIndexLookup(name) {
693
- | Some(lookup) => lookup(filterField, value)->scoped->JSON.Encode.array
890
+ | Some(lookup) => indexConnection(~args, lookup(filterField, value)->scoped)
694
891
  | None =>
695
892
  switch Bus.getQueryDbScan(name) {
696
893
  | Some(scanAll) =>
697
- scanAll()
698
- ->Array.filter(item =>
699
- item
700
- ->JSON.Decode.object
701
- ->Option.flatMap(d => d->Dict.get(filterField))
702
- ->Option.flatMap(JSON.Decode.string)
703
- ->Option.map(v => v == value)
704
- ->Option.getOr(false)
894
+ indexConnection(
895
+ ~args,
896
+ scanAll()
897
+ ->Array.filter(item =>
898
+ item
899
+ ->JSON.Decode.object
900
+ ->Option.flatMap(d => d->Dict.get(filterField))
901
+ ->Option.flatMap(JSON.Decode.string)
902
+ ->Option.map(v => v == value)
903
+ ->Option.getOr(false)
904
+ )
905
+ ->scoped,
705
906
  )
706
- ->scoped
707
- ->JSON.Encode.array
708
- | None => []->JSON.Encode.array
907
+ | None => indexConnection(~args, [])
709
908
  }
710
909
  }
711
910
  }
@@ -718,6 +917,7 @@ module Make = (Bus: LocalBus.T) => {
718
917
  let allSdl =
719
918
  [byIdSdl]
720
919
  ->Array.concat(byIdsSdl)
920
+ ->Array.concat(refsSdl)
721
921
  ->Array.concat(listSdl)
722
922
  ->Array.concat(itemsSdl)
723
923
  ->Array.concat(indexSdlFields)
@@ -725,6 +925,7 @@ module Make = (Bus: LocalBus.T) => {
725
925
  let resolvers = Dict.make()
726
926
  resolvers->Dict.set(singleQueryName, byIdResolver)
727
927
  byIdsResolverEntry->Option.forEach(((k, v)) => resolvers->Dict.set(k, v))
928
+ refsResolverEntry->Option.forEach(((k, v)) => resolvers->Dict.set(k, v))
728
929
  listResolvers->Array.forEach(((k, v)) => resolvers->Dict.set(k, v))
729
930
  itemsResolvers->Array.forEach(((k, v)) => resolvers->Dict.set(k, v))
730
931
  indexResolvers->Array.forEach(((k, v)) => resolvers->Dict.set(k, v))
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
4
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
4
5
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
6
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
7
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
@@ -16,6 +17,7 @@ import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/s
16
17
  import * as SortKey_Filter$ReventlessLocal from "./SortKey_Filter.res.mjs";
17
18
  import * as QueryDbListQuery$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/QueryDbListQuery.res.mjs";
18
19
  import * as QueryDb_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Callback.res.mjs";
20
+ import * as GraphQL_CallerError$ReventlessLocal from "../GraphQL_CallerError.res.mjs";
19
21
  import * as DomainGraphQL_Server$ReventlessLocal from "../DomainGraphQL_Server.res.mjs";
20
22
  import * as OwnerScopeDiagnostics$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/OwnerScopeDiagnostics.res.mjs";
21
23
  import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
@@ -112,7 +114,6 @@ function Make(Bus) {
112
114
  return true;
113
115
  }
114
116
  };
115
- let cap = s => s.charAt(0).toUpperCase() + s.slice(1);
116
117
  let registryEntry = Plugin_Helpers$ReventlessCore.queryFieldNamesRegistry[name];
117
118
  let singleQueryName = registryEntry !== undefined ? registryEntry.singleFieldName : name.charAt(0).toLowerCase() + name.slice(1);
118
119
  let listQueryName = registryEntry !== undefined ? registryEntry.listFieldName : name + "s";
@@ -124,7 +125,7 @@ function Make(Bus) {
124
125
  relay.registerNodeType(returnTypeName, name);
125
126
  }
126
127
  let byIdSdl = includeIdParam ? (
127
- subIdField !== undefined ? ` ` + singleQueryName + `(id: ID!, ` + subIdField + `: String): ` + returnTypeName : ` ` + singleQueryName + `(id: ID!): ` + returnTypeName
128
+ subIdField !== undefined ? ` ` + singleQueryName + `(id: ID!, ` + subIdField + `: String, includeRetired: Boolean): ` + returnTypeName : ` ` + singleQueryName + `(id: ID!, includeRetired: Boolean): ` + returnTypeName
128
129
  ) : ` ` + singleQueryName + `: ` + returnTypeName;
129
130
  let byIdResolver = async (_root, args, ctx) => {
130
131
  let match = await runInterceptor(ctx, args);
@@ -219,6 +220,101 @@ function Make(Bus) {
219
220
  byIdsResolverEntry = undefined;
220
221
  }
221
222
  let labelField = registryEntry !== undefined ? Stdlib_Option.getOr(registryEntry.labelField, "id") : "id";
223
+ let refsSdl = includeIdParam && subIdField === undefined ? [GraphQL_FragmentGenerator$ReventlessCore.deriveRefsQueryField(listQueryName, returnTypeName)] : [];
224
+ let refsResolverEntry;
225
+ if (includeIdParam && subIdField === undefined) {
226
+ server.registerTypes([GraphQL_FragmentGenerator$ReventlessCore.deriveRefTypeSdl(returnTypeName)]);
227
+ let resolver$1 = async (_root, args, ctx) => {
228
+ let match = await runInterceptor(ctx, args);
229
+ if (typeof match === "object") {
230
+ return [];
231
+ }
232
+ let ids = Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d["ids"]), Stdlib_JSON.Decode.array), []), Stdlib_JSON.Decode.string);
233
+ let spec = retiredSpecOf();
234
+ let namesRetired = Stdlib_Option.mapOr(spec, false, r => r.namedWhenRetired);
235
+ let retirementOf = item => {
236
+ if (spec === undefined) {
237
+ return [
238
+ false,
239
+ undefined
240
+ ];
241
+ }
242
+ let cell = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[spec.field]);
243
+ let scope_field = spec.field;
244
+ let scope_values = spec.values;
245
+ let scope = {
246
+ field: scope_field,
247
+ values: scope_values
248
+ };
249
+ let retired = OwnerScope$Reventless.isRetiredValue(scope, cell);
250
+ return [
251
+ retired,
252
+ retired && Stdlib_Option.isSome(spec.values) ? Stdlib_Option.flatMap(cell, Stdlib_JSON.Decode.string) : undefined
253
+ ];
254
+ };
255
+ let ops = Bus.getQueryDb(name);
256
+ if (ops === undefined) {
257
+ return [];
258
+ }
259
+ let load = key => Effect.runPromise(Effect.catchAll(Stream.runCollect(ops.loadStream(key)), param => Effect.succeed([])));
260
+ let loaded = await Promise.all(ids.map(async id => {
261
+ let item = (await load(id))[0];
262
+ if (item !== undefined) {
263
+ return [
264
+ id,
265
+ item
266
+ ];
267
+ }
268
+ let localId = Api_Ids$ReventlessCore.alternateKey(id);
269
+ if (localId !== undefined) {
270
+ return [
271
+ localId,
272
+ (await load(localId))[0]
273
+ ];
274
+ } else {
275
+ return [
276
+ id,
277
+ undefined
278
+ ];
279
+ }
280
+ }));
281
+ return Stdlib_Array.filterMap(loaded, param => {
282
+ let id = param[0];
283
+ return Stdlib_Option.flatMap(Stdlib_Option.filter(param[1], item => ownerAllows(ctx, item)), item => {
284
+ let match = retirementOf(item);
285
+ let retired = match[0];
286
+ if (retired && !namesRetired) {
287
+ return;
288
+ }
289
+ let label = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[labelField]), Stdlib_JSON.Decode.string), id);
290
+ return Object.fromEntries([
291
+ [
292
+ "id",
293
+ id
294
+ ],
295
+ [
296
+ "label",
297
+ label
298
+ ],
299
+ [
300
+ "retired",
301
+ retired
302
+ ],
303
+ [
304
+ "retiredState",
305
+ Stdlib_Option.mapOr(match[1], null, prim => prim)
306
+ ]
307
+ ]);
308
+ });
309
+ });
310
+ };
311
+ refsResolverEntry = [
312
+ listQueryName + "Refs",
313
+ resolver$1
314
+ ];
315
+ } else {
316
+ refsResolverEntry = undefined;
317
+ }
222
318
  let stateSchemaOpt = Plugin_Helpers$ReventlessCore.stateSchemaRegistry[name];
223
319
  let capability = stateSchemaOpt !== undefined ? GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(name, stateSchemaOpt) : GraphQL_FragmentGenerator$ReventlessCore.emptyCapability;
224
320
  let fetchAllItems = async () => {
@@ -250,7 +346,7 @@ function Make(Bus) {
250
346
  endCursor: null
251
347
  }
252
348
  };
253
- let resolver$1 = async (_root, args, ctx) => {
349
+ let resolver$2 = async (_root, args, ctx) => {
254
350
  let match = await runInterceptor(ctx, args);
255
351
  if (typeof match === "object") {
256
352
  return emptyConnection;
@@ -277,11 +373,11 @@ function Make(Bus) {
277
373
  };
278
374
  match = [
279
375
  sdl,
280
- resolver$1
376
+ resolver$2
281
377
  ];
282
378
  } else {
283
379
  let sdl$1 = [` ` + listQueryName + `(nextToken: String, limit: Int): ` + pluralTypeName + `!`];
284
- let resolver$2 = async (_root, args, ctx) => {
380
+ let resolver$3 = async (_root, args, ctx) => {
285
381
  let match = await runInterceptor(ctx, args);
286
382
  if (typeof match === "object") {
287
383
  return {
@@ -306,7 +402,7 @@ function Make(Bus) {
306
402
  };
307
403
  match = [
308
404
  sdl$1,
309
- resolver$2
405
+ resolver$3
310
406
  ];
311
407
  }
312
408
  let listResolvers = [[
@@ -324,7 +420,7 @@ function Make(Bus) {
324
420
  }
325
421
  let itemsResolvers;
326
422
  if (subIdField !== undefined) {
327
- let resolver$3 = async (_root, args, ctx) => {
423
+ let resolver$4 = async (_root, args, ctx) => {
328
424
  let emptyConn = {
329
425
  edges: [],
330
426
  pageInfo: {
@@ -423,22 +519,81 @@ function Make(Bus) {
423
519
  };
424
520
  itemsResolvers = [[
425
521
  singleQueryName + "Items",
426
- resolver$3
522
+ resolver$4
427
523
  ]];
428
524
  } else {
429
525
  itemsResolvers = [];
430
526
  }
431
- let indexSdlFields = indexes.map(ic => ` ` + singleQueryName + `By` + cap(ic.index) + `(` + ic.index + `: String!): [String]`);
527
+ let indexSdlFields = indexes.map(ic => GraphQL_FragmentGenerator$ReventlessCore.deriveIndexQueryField(singleQueryName, ic, returnTypeName + "Connection"));
528
+ let rejectBackwardPaging = args => {
529
+ let given = key => {
530
+ let match = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d[key]);
531
+ if (match !== undefined) {
532
+ return match !== null;
533
+ } else {
534
+ return false;
535
+ }
536
+ };
537
+ if (!(given("last") || given("before"))) {
538
+ return;
539
+ }
540
+ throw GraphQL_CallerError$ReventlessLocal.badUserInput("Backward pagination (last/before) is not supported on by-index connections; use first/after.");
541
+ };
542
+ let indexConnection = (args, items) => {
543
+ let arg = key => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d[key]);
544
+ let after = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(arg("after"), Stdlib_JSON.Decode.string), s => Stdlib_Int.fromString(s, undefined)), -1);
545
+ let first = Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(arg("first"), Stdlib_JSON.Decode.float), prim => prim | 0), 50);
546
+ let start = after + 1 | 0;
547
+ let page = items.slice(start, start + first | 0);
548
+ let taken = page.length;
549
+ let edges = page.map((item, i) => Object.fromEntries([
550
+ [
551
+ "node",
552
+ item
553
+ ],
554
+ [
555
+ "cursor",
556
+ (start + i | 0).toString()
557
+ ]
558
+ ]));
559
+ return Object.fromEntries([
560
+ [
561
+ "edges",
562
+ edges
563
+ ],
564
+ [
565
+ "pageInfo",
566
+ Object.fromEntries([
567
+ [
568
+ "hasNextPage",
569
+ (start + taken | 0) < items.length
570
+ ],
571
+ [
572
+ "hasPreviousPage",
573
+ start > 0
574
+ ],
575
+ [
576
+ "startCursor",
577
+ taken > 0 ? start.toString() : null
578
+ ],
579
+ [
580
+ "endCursor",
581
+ taken > 0 ? ((start + taken | 0) - 1 | 0).toString() : null
582
+ ]
583
+ ])
584
+ ]
585
+ ]);
586
+ };
432
587
  let indexResolvers = indexes.map(ic => {
433
- let index = ic.index;
434
- let resolverName = singleQueryName + "By" + cap(index);
435
- let filterField = Stdlib_Option.getOr(ic.idField, index);
588
+ let resolverName = GraphQL_FragmentGenerator$ReventlessCore.indexQueryFieldName(singleQueryName, ic.index);
589
+ let filterField = GraphQL_FragmentGenerator$ReventlessCore.indexKeyField(ic);
436
590
  let resolver = async (_root, args, ctx) => {
437
591
  let match = await runInterceptor(ctx, args);
438
592
  if (typeof match === "object") {
439
- return [];
593
+ return indexConnection(args, []);
440
594
  }
441
- let value = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d[index]), Stdlib_JSON.Decode.string), "");
595
+ rejectBackwardPaging(args);
596
+ let value = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d[filterField]), Stdlib_JSON.Decode.string), "");
442
597
  let scoped = rows => rows.filter(item => {
443
598
  if (ownerAllows(ctx, item)) {
444
599
  return retiredAllows(ctx, args, item);
@@ -448,13 +603,13 @@ function Make(Bus) {
448
603
  });
449
604
  let lookup = Bus.getQueryDbIndexLookup(name);
450
605
  if (lookup !== undefined) {
451
- return scoped(lookup(filterField, value));
606
+ return indexConnection(args, scoped(lookup(filterField, value)));
452
607
  }
453
608
  let scanAll = Bus.getQueryDbScan(name);
454
609
  if (scanAll !== undefined) {
455
- return scoped(scanAll().filter(item => Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[filterField]), Stdlib_JSON.Decode.string), v => v === value), false)));
610
+ return indexConnection(args, scoped(scanAll().filter(item => Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[filterField]), Stdlib_JSON.Decode.string), v => v === value), false))));
456
611
  } else {
457
- return [];
612
+ return indexConnection(args, []);
458
613
  }
459
614
  };
460
615
  return [
@@ -462,12 +617,15 @@ function Make(Bus) {
462
617
  resolver
463
618
  ];
464
619
  });
465
- let allSdl = [byIdSdl].concat(byIdsSdl).concat(match[0]).concat(itemsSdl).concat(indexSdlFields);
620
+ let allSdl = [byIdSdl].concat(byIdsSdl).concat(refsSdl).concat(match[0]).concat(itemsSdl).concat(indexSdlFields);
466
621
  let resolvers = {};
467
622
  resolvers[singleQueryName] = byIdResolver;
468
623
  Stdlib_Option.forEach(byIdsResolverEntry, param => {
469
624
  resolvers[param[0]] = param[1];
470
625
  });
626
+ Stdlib_Option.forEach(refsResolverEntry, param => {
627
+ resolvers[param[0]] = param[1];
628
+ });
471
629
  listResolvers.forEach(param => {
472
630
  resolvers[param[0]] = param[1];
473
631
  });