@reventlessdev/reventless-local 3.0.0-alpha.225 → 3.0.0-alpha.227

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 CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.227 (2026-08-18)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **api:** declare the reference door in the SDL every backend is built from ([5c1857e](https://github.com/ReventlessDev/reventless-core/commit/5c1857ea90ff40305a1c44a9e57043528e6a93aa))
11
+ * **api:** make the by-index door answer, and let an elevated caller widen it ([0fe0c6f](https://github.com/ReventlessDev/reventless-core/commit/0fe0c6f8dec6228ecaba39577e28d780b4f79c83))
12
+ ### Features
13
+
14
+ * **core:** let a reference name a retired row, and let an elevated caller open one ([9e2623a](https://github.com/ReventlessDev/reventless-core/commit/9e2623a4b22487561607fcc0ca19d51726069ee4))
15
+
16
+
17
+ # 3.0.0-alpha.226 (2026-08-16)
18
+
19
+ **Note:** Version bump only for package @reventlessdev/reventless-local
20
+
21
+
22
+
23
+
24
+
6
25
  # 3.0.0-alpha.225 (2026-08-16)
7
26
 
8
27
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.225",
3
+ "version": "3.0.0-alpha.227",
4
4
  "description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -36,22 +36,22 @@
36
36
  "ws": "^8.18.0",
37
37
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
38
38
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.27",
39
- "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
40
39
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
41
- "@reventlessdev/rescript-node": "2.0.0-alpha.7",
40
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
41
+ "@reventlessdev/rescript-node": "2.0.0-alpha.8",
42
+ "@reventlessdev/reventless-core": "3.0.0-alpha.239",
42
43
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
43
- "@reventlessdev/reventless-core": "3.0.0-alpha.237",
44
- "@reventlessdev/reventless-infra": "3.0.0-alpha.143",
45
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.85",
46
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.184",
47
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.101",
48
- "@reventlessdev/reventless-spec": "3.0.0-alpha.115",
49
- "@reventlessdev/reventless-seed": "1.0.0-alpha.15"
44
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.186",
45
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.87",
46
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.145",
47
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.103",
48
+ "@reventlessdev/reventless-seed": "1.0.0-alpha.16",
49
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.117"
50
50
  },
51
51
  "devDependencies": {
52
52
  "rescript": "12.3.0",
53
53
  "sury-ppx": "11.0.0-alpha.2",
54
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.67"
54
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.69"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "rescript": "12.3.0"
package/src/Platform.res CHANGED
@@ -700,7 +700,7 @@ module MakeWithConfig = (
700
700
  StateViewSliceMaker.Make(Spec, Projection)
701
701
  }
702
702
 
703
- // Admin UI-fragment registry (docs/plans/event-sourced-fragment-registries.md): the
703
+ // Admin UI-fragment registry (docs/plans/done/event-sourced-fragment-registries.md): the
704
704
  // platform UI-fragment registry hosted as an admin DCB slice on the admin DcbEventLog.
705
705
  // Built once and passed into Admin.construct below.
706
706
  module UiFragmentRegistrySlice = StateChangeSlice.Make(
@@ -1428,11 +1428,10 @@ module MakeWithConfig = (
1428
1428
  switch entry.indexQueries {
1429
1429
  | Some(indexes) =>
1430
1430
  indexes->Array.forEach((ic: Reventless.ReadModel.indexConfig) => {
1431
- let stripped =
1432
- ic.index->String.startsWith("by") && ic.index->String.length > 2
1433
- ? ic.index->String.slice(~start=2, ~end=ic.index->String.length)
1434
- : ic.index
1435
- let fieldName = entry.singleFieldName ++ "By" ++ stripped->String.capitalize
1431
+ let fieldName = ReventlessCore.GraphQL_FragmentGenerator.indexQueryFieldName(
1432
+ ~singleFieldName=entry.singleFieldName,
1433
+ ~index=ic.index,
1434
+ )
1436
1435
  queryResolvers->Dict.set(fieldName, async (_root, _args, _ctx): JSON.t =>
1437
1436
  connectionResponse([])
1438
1437
  )
@@ -9,7 +9,6 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
9
9
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
10
10
  import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
11
11
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
12
- import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
13
12
  import * as Effect from "effect/Effect";
14
13
  import * as Pulumi from "@pulumi/pulumi";
15
14
  import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
@@ -81,6 +80,7 @@ import * as LocalDcbEventLogStorage$ReventlessLocal from "./adapter/DcbEventLog/
81
80
  import * as LocalRuntimeEnvironment$ReventlessLocal from "./adapter/Runtime/LocalRuntimeEnvironment.res.mjs";
82
81
  import * as LocalScheduledPublisher$ReventlessLocal from "./adapter/Scheduler/LocalScheduledPublisher.res.mjs";
83
82
  import * as Platform_Admin_Structure$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin_Structure.res.mjs";
83
+ import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
84
84
  import * as LocalCommandTopicChannel$ReventlessLocal from "./adapter/CommandTopic/LocalCommandTopicChannel.res.mjs";
85
85
  import * as LocalEventTopicPublisher$ReventlessLocal from "./adapter/EventTopic/LocalEventTopicPublisher.res.mjs";
86
86
  import * as StateChangeSlice_Builder$ReventlessLocal from "./components/StateChangeSlice_Builder.res.mjs";
@@ -1206,8 +1206,7 @@ function MakeWithConfig(Config) {
1206
1206
  let indexes = entry.indexQueries;
1207
1207
  if (indexes !== undefined) {
1208
1208
  indexes.forEach(ic => {
1209
- let stripped = ic.index.startsWith("by") && ic.index.length > 2 ? ic.index.slice(2, ic.index.length) : ic.index;
1210
- let fieldName = entry.singleFieldName + "By" + Stdlib_String.capitalize(stripped);
1209
+ let fieldName = GraphQL_FragmentGenerator$ReventlessCore.indexQueryFieldName(entry.singleFieldName, ic.index);
1211
1210
  queryResolvers[fieldName] = async (_root, _args, _ctx) => connectionResponse([]);
1212
1211
  });
1213
1212
  return;
@@ -2933,8 +2932,7 @@ function Make($star) {
2933
2932
  let indexes = entry.indexQueries;
2934
2933
  if (indexes !== undefined) {
2935
2934
  indexes.forEach(ic => {
2936
- let stripped = ic.index.startsWith("by") && ic.index.length > 2 ? ic.index.slice(2, ic.index.length) : ic.index;
2937
- let fieldName = entry.singleFieldName + "By" + Stdlib_String.capitalize(stripped);
2935
+ let fieldName = GraphQL_FragmentGenerator$ReventlessCore.indexQueryFieldName(entry.singleFieldName, ic.index);
2938
2936
  queryResolvers[fieldName] = async (_root, _args, _ctx) => connectionResponse([]);
2939
2937
  });
2940
2938
  return;
@@ -20,7 +20,7 @@ let ensureSchema = (db: SqliteDriver.t) => {
20
20
  db->SqliteDriver.exec(
21
21
  "CREATE TABLE IF NOT EXISTS event_log (log_name TEXT NOT NULL, aggregate_id TEXT NOT NULL, seq_nr INTEGER NOT NULL, payload TEXT NOT NULL, PRIMARY KEY (log_name, aggregate_id, seq_nr))",
22
22
  )
23
- // Keep-one aggregate-state snapshots (docs/plans/aggregate-snapshotting.md):
23
+ // Keep-one aggregate-state snapshots (docs/plans/done/aggregate-snapshotting.md):
24
24
  // one row per (log, aggregate), overwritten on every snapshot write. `state`
25
25
  // is the JSON-encoded fold of events seq 0..seq_nr-1; `schema_hash` lets the
26
26
  // consumer detect a state-schema drift and fall back to full replay.
@@ -9,7 +9,7 @@
9
9
  // refactor. So under Backend.Postgres the read-model live-query path uses the
10
10
  // in-memory arm (getDb() is None for Postgres → Memory), rebuilt by replay from
11
11
  // the durable pg event log; the deploy-usable ReventlessPostgres.QueryDbStorage_Postgres
12
- // serves non-local compute layers. See docs/plans/postgres-storage-adapter.md §E.
12
+ // serves non-local compute layers. See docs/plans/done/postgres-storage-adapter.md §E.
13
13
 
14
14
  module Make = (Bus: LocalBus.T) => {
15
15
  module Mem = QueryDbStorage_InMemory.Make(Bus)
@@ -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))