@reventlessdev/reventless-local 3.0.0-alpha.233 → 3.0.0-alpha.234

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,14 @@
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.234 (2026-08-21)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **core:** make @resolves/[@resolves](https://github.com/resolves)Many work end to end ([4c52957](https://github.com/ReventlessDev/reventless-core/commit/4c5295759dcb4b3eda4f0f4f2c1bc387fed88fcb))
11
+ * **deps:** update sury to 11.0.0-rc.2 to fix unreachable union constructors ([fa5744f](https://github.com/ReventlessDev/reventless-core/commit/fa5744fed8de975e2f14725c856c6e5ce7d04a74))
12
+
13
+
6
14
  # 3.0.0-alpha.233 (2026-08-20)
7
15
 
8
16
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.233",
3
+ "version": "3.0.0-alpha.234",
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": {
@@ -32,25 +32,25 @@
32
32
  "graphql": "^16.13.2",
33
33
  "graphql-ws": "^5.16.0",
34
34
  "graphql-yoga": "^5.21.0",
35
- "sury": "11.0.0-rc.1",
35
+ "sury": "11.0.0-rc.2",
36
36
  "ws": "^8.18.0",
37
- "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.27",
38
37
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
39
- "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
40
- "@reventlessdev/rescript-node": "2.0.0-alpha.8",
38
+ "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.27",
41
39
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
40
+ "@reventlessdev/rescript-node": "2.0.0-alpha.8",
41
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
42
42
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
43
- "@reventlessdev/reventless-core": "3.0.0-alpha.245",
44
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.192",
43
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.94",
44
+ "@reventlessdev/reventless-core": "3.0.0-alpha.246",
45
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.149",
46
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.110",
45
47
  "@reventlessdev/reventless-seed": "1.0.0-alpha.17",
46
- "@reventlessdev/reventless-spec": "3.0.0-alpha.120",
47
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.109",
48
- "@reventlessdev/reventless-infra": "3.0.0-alpha.148",
49
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.93"
48
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.193",
49
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.121"
50
50
  },
51
51
  "devDependencies": {
52
52
  "rescript": "12.3.0",
53
- "sury-ppx": "11.0.0-rc.1",
53
+ "sury-ppx": "11.0.0-rc.2",
54
54
  "@reventlessdev/reventless-ppx": "1.0.0-alpha.72"
55
55
  },
56
56
  "peerDependencies": {
@@ -347,6 +347,10 @@ type bucket = {
347
347
  mutationResolvers: dict<resolverFn>,
348
348
  queryResolvers: dict<resolverFn>,
349
349
  subscriptionResolvers: dict<resolverFn>,
350
+ // Object-type field resolvers, keyed type name → field name. Carries the
351
+ // cross-table fields `@resolves` / `@resolvesMany` add to a queryable; their
352
+ // SDL rides the plugin's schema fragment, so only the map lives here.
353
+ fieldResolvers: dict<dict<resolverFn>>,
350
354
  }
351
355
 
352
356
  let makeBucket = (~scope: string): bucket => {
@@ -363,6 +367,7 @@ let makeBucket = (~scope: string): bucket => {
363
367
  mutationResolvers: Dict.make(),
364
368
  queryResolvers: Dict.make(),
365
369
  subscriptionResolvers: Dict.make(),
370
+ fieldResolvers: Dict.make(),
366
371
  }
367
372
 
368
373
  // Insertion order = composition order (platform bucket is forced first in
@@ -420,6 +425,15 @@ let relabelScope = (~from: string, ~to_: string) =>
420
425
  b.subscriptionResolvers
421
426
  ->Dict.toArray
422
427
  ->Array.forEach(((k, v)) => existing.subscriptionResolvers->Dict.set(k, v))
428
+ b.fieldResolvers
429
+ ->Dict.toArray
430
+ ->Array.forEach(((typeName, byField)) =>
431
+ switch existing.fieldResolvers->Dict.get(typeName) {
432
+ | Some(target) =>
433
+ byField->Dict.toArray->Array.forEach(((k, v)) => target->Dict.set(k, v))
434
+ | None => existing.fieldResolvers->Dict.set(typeName, byField)
435
+ }
436
+ )
423
437
  }
424
438
  if currentScope.contents == from {
425
439
  currentScope.contents = to_
@@ -453,6 +467,24 @@ let registerSubscriptions = (~sdlFields: array<string>, ~resolvers: dict<resolve
453
467
  resolvers->Dict.toArray->Array.forEach(((k, v)) => b.subscriptionResolvers->Dict.set(k, v))
454
468
  }
455
469
 
470
+ let registerFieldResolvers = (~typeName: string, ~resolvers: dict<resolverFn>) => {
471
+ let b = currentBucket()
472
+ let forType = switch b.fieldResolvers->Dict.get(typeName) {
473
+ | Some(d) => d
474
+ | None =>
475
+ let d = Dict.make()
476
+ b.fieldResolvers->Dict.set(typeName, d)
477
+ d
478
+ }
479
+ resolvers->Dict.toArray->Array.forEach(((k, v)) => forType->Dict.set(k, v))
480
+ }
481
+
482
+ /** Look up a registered object-type field resolver. Searches all scope buckets. */
483
+ let getFieldResolver = (~typeName: string, fieldName: string): option<resolverFn> =>
484
+ buckets.contents
485
+ ->Dict.valuesToArray
486
+ ->Array.findMap(b => b.fieldResolvers->Dict.get(typeName)->Option.flatMap(d => d->Dict.get(fieldName)))
487
+
456
488
  /** Look up a registered mutation resolver by field name (used by MCP_Server).
457
489
  Searches all scope buckets. */
458
490
  let getMutationResolver = (fieldName: string): option<resolverFn> =>
@@ -620,6 +652,9 @@ let scopeResolverMap = (b: bucket, ~withRootDefaults: bool): dict<dict<resolverF
620
652
  if b.subscriptionResolvers->Dict.keysToArray->Array.length > 0 {
621
653
  resolvers->Dict.set("Subscription", b.subscriptionResolvers)
622
654
  }
655
+ b.fieldResolvers
656
+ ->Dict.toArray
657
+ ->Array.forEach(((typeName, byField)) => resolvers->Dict.set(typeName, byField))
623
658
  resolvers
624
659
  }
625
660
 
@@ -913,6 +948,7 @@ let asInterface: ReventlessGraphqlServer.GraphQL_ServerInstance.t = {
913
948
  registerQueries,
914
949
  registerSubscriptions,
915
950
  registerTypes,
951
+ registerFieldResolvers,
916
952
  getMutationResolver,
917
953
  getQueryResolver,
918
954
  start,
@@ -252,7 +252,8 @@ function makeBucket(scope) {
252
252
  typeDefinitions: scope === platformScope ? [] : GraphQL_Stitcher$ReventlessCore.relayBaseTypes.slice(),
253
253
  mutationResolvers: {},
254
254
  queryResolvers: {},
255
- subscriptionResolvers: {}
255
+ subscriptionResolvers: {},
256
+ fieldResolvers: {}
256
257
  };
257
258
  }
258
259
 
@@ -316,6 +317,18 @@ function relabelScope(from, to_) {
316
317
  Object.entries(b.subscriptionResolvers).forEach(param => {
317
318
  existing.subscriptionResolvers[param[0]] = param[1];
318
319
  });
320
+ Object.entries(b.fieldResolvers).forEach(param => {
321
+ let byField = param[1];
322
+ let typeName = param[0];
323
+ let target = existing.fieldResolvers[typeName];
324
+ if (target !== undefined) {
325
+ Object.entries(byField).forEach(param => {
326
+ target[param[0]] = param[1];
327
+ });
328
+ } else {
329
+ existing.fieldResolvers[typeName] = byField;
330
+ }
331
+ });
319
332
  } else {
320
333
  buckets.contents[to_] = b;
321
334
  }
@@ -354,6 +367,26 @@ function registerSubscriptions(sdlFields, resolvers) {
354
367
  });
355
368
  }
356
369
 
370
+ function registerFieldResolvers(typeName, resolvers) {
371
+ let b = bucketFor(currentScope.contents);
372
+ let d = b.fieldResolvers[typeName];
373
+ let forType;
374
+ if (d !== undefined) {
375
+ forType = d;
376
+ } else {
377
+ let d$1 = {};
378
+ b.fieldResolvers[typeName] = d$1;
379
+ forType = d$1;
380
+ }
381
+ Object.entries(resolvers).forEach(param => {
382
+ forType[param[0]] = param[1];
383
+ });
384
+ }
385
+
386
+ function getFieldResolver(typeName, fieldName) {
387
+ return Stdlib_Array.findMap(Object.values(buckets.contents), b => Stdlib_Option.flatMap(b.fieldResolvers[typeName], d => d[fieldName]));
388
+ }
389
+
357
390
  function getMutationResolver(fieldName) {
358
391
  return Stdlib_Array.findMap(Object.values(buckets.contents), b => b.mutationResolvers[fieldName]);
359
392
  }
@@ -486,6 +519,9 @@ function scopeResolverMap(b, withRootDefaults) {
486
519
  if (Object.keys(b.subscriptionResolvers).length !== 0) {
487
520
  resolvers["Subscription"] = b.subscriptionResolvers;
488
521
  }
522
+ Object.entries(b.fieldResolvers).forEach(param => {
523
+ resolvers[param[0]] = param[1];
524
+ });
489
525
  return resolvers;
490
526
  }
491
527
 
@@ -749,6 +785,7 @@ let asInterface = {
749
785
  registerQueries: registerQueries,
750
786
  registerSubscriptions: registerSubscriptions,
751
787
  registerTypes: registerTypes,
788
+ registerFieldResolvers: registerFieldResolvers,
752
789
  getMutationResolver: getMutationResolver,
753
790
  getQueryResolver: getQueryResolver,
754
791
  start: start,
@@ -798,6 +835,8 @@ export {
798
835
  registerMutations,
799
836
  registerQueries,
800
837
  registerSubscriptions,
838
+ registerFieldResolvers,
839
+ getFieldResolver,
801
840
  getMutationResolver,
802
841
  getQueryResolver,
803
842
  registerTypes,
@@ -62,6 +62,10 @@ let registerQueries = DomainGraphQL_Server$ReventlessLocal.registerQueries;
62
62
 
63
63
  let registerSubscriptions = DomainGraphQL_Server$ReventlessLocal.registerSubscriptions;
64
64
 
65
+ let registerFieldResolvers = DomainGraphQL_Server$ReventlessLocal.registerFieldResolvers;
66
+
67
+ let getFieldResolver = DomainGraphQL_Server$ReventlessLocal.getFieldResolver;
68
+
65
69
  let getMutationResolver = DomainGraphQL_Server$ReventlessLocal.getMutationResolver;
66
70
 
67
71
  let getQueryResolver = DomainGraphQL_Server$ReventlessLocal.getQueryResolver;
@@ -157,6 +161,8 @@ export {
157
161
  registerMutations,
158
162
  registerQueries,
159
163
  registerSubscriptions,
164
+ registerFieldResolvers,
165
+ getFieldResolver,
160
166
  getMutationResolver,
161
167
  getQueryResolver,
162
168
  registerTypes,
@@ -48,6 +48,10 @@ let registerQueries = (~sdlFields, ~resolvers) =>
48
48
  instance.registerQueries(~sdlFields, ~resolvers=wrapAdmin(resolvers))
49
49
  let registerSubscriptions = instance.registerSubscriptions
50
50
  let registerTypes = instance.registerTypes
51
+ // Not admin-wrapped: a field resolver is only reached through a parent field
52
+ // that already met the group check, so wrapping would re-refuse a caller the
53
+ // root already admitted.
54
+ let registerFieldResolvers = instance.registerFieldResolvers
51
55
  let getMutationResolver = instance.getMutationResolver
52
56
  let getQueryResolver = instance.getQueryResolver
53
57
  let start = instance.start
@@ -67,6 +71,7 @@ let asInterface: ReventlessGraphqlServer.GraphQL_ServerInstance.t = {
67
71
  registerQueries,
68
72
  registerSubscriptions,
69
73
  registerTypes,
74
+ registerFieldResolvers,
70
75
  getMutationResolver,
71
76
  getQueryResolver,
72
77
  start,
@@ -25,6 +25,8 @@ let registerSubscriptions = instance.registerSubscriptions;
25
25
 
26
26
  let registerTypes = instance.registerTypes;
27
27
 
28
+ let registerFieldResolvers = instance.registerFieldResolvers;
29
+
28
30
  let getMutationResolver = instance.getMutationResolver;
29
31
 
30
32
  let getQueryResolver = instance.getQueryResolver;
@@ -50,6 +52,7 @@ let asInterface = {
50
52
  registerQueries: registerQueries,
51
53
  registerSubscriptions: registerSubscriptions,
52
54
  registerTypes: registerTypes,
55
+ registerFieldResolvers: registerFieldResolvers,
53
56
  getMutationResolver: getMutationResolver,
54
57
  getQueryResolver: getQueryResolver,
55
58
  start: start,
@@ -69,6 +72,7 @@ export {
69
72
  registerQueries,
70
73
  registerSubscriptions,
71
74
  registerTypes,
75
+ registerFieldResolvers,
72
76
  getMutationResolver,
73
77
  getQueryResolver,
74
78
  start,
@@ -72,8 +72,8 @@ module Make = (Bus: LocalBus.T) => {
72
72
  ~dataSourceName as _,
73
73
  ~indexes,
74
74
  ~subIdField,
75
- ~idResolverConfigs as _,
76
- ~idsResolverConfigs as _,
75
+ ~idResolverConfigs,
76
+ ~idsResolverConfigs,
77
77
  ~authorization,
78
78
  ~opts as _,
79
79
  ) => {
@@ -913,6 +913,175 @@ module Make = (Bus: LocalBus.T) => {
913
913
  },
914
914
  )
915
915
 
916
+ // -- Cross-table fields: @resolves / @resolvesMany -------------------------
917
+ // Field resolvers on this view's object type, following a foreign key into
918
+ // another queryable's rows. Their SDL rides the plugin's schema fragment
919
+ // (`GraphQL_FragmentGenerator` emits it from the same config), so only the
920
+ // resolvers are registered here.
921
+ //
922
+ // The rows are the TARGET's, so the target's `@owner` / `@retired` rules
923
+ // narrow them — the same reading the AppSync resolver bakes into its
924
+ // response. A nested field takes no `includeRetired` argument, so a retired
925
+ // row never travels through one; `{list}Refs` is the door that names those.
926
+ let targetAllows = (~ctx, ~target: string, item: JSON.t): bool => {
927
+ let schema = Plugin_Helpers.stateSchemaRegistry->Dict.get(target)
928
+ let cell = field => item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(field))
929
+ let identity = extractIdentity(ctx)
930
+ let ownerOk = switch identity->Reventless.OwnerScope.decide(
931
+ ~ownerField=schema->Option.flatMap(s => Reventless.Owner.fieldNames(s)->Array.get(0)),
932
+ ) {
933
+ | Unscoped => true
934
+ | RefuseOwned => false
935
+ | ScopeTo(field, required) =>
936
+ cell(field)->Option.flatMap(JSON.Decode.string)->Option.mapOr(false, v => v == required)
937
+ }
938
+ let retiredSpec =
939
+ schema
940
+ ->Option.flatMap(Reventless.StateAnnotations.getSpec)
941
+ ->Option.flatMap(spec => spec.retired)
942
+ let retiredOk = switch identity
943
+ ->Reventless.OwnerScope.decideRetired(
944
+ ~retiredField=retiredSpec->Option.map(r => r.field),
945
+ ~retiredValues=?retiredSpec->Option.flatMap(r => r.values),
946
+ ~asked=false,
947
+ )
948
+ ->Reventless.OwnerScope.retiredScopeOf {
949
+ | None => true
950
+ | Some(scope) => !(scope->Reventless.OwnerScope.isRetiredValue(cell(scope.field)))
951
+ }
952
+ ownerOk && retiredOk
953
+ }
954
+
955
+ // The storage key the row was loaded by, reported as `id` — the nested type
956
+ // is a `Node`, so the field is non-null, and `loadStream` hands back the
957
+ // stored value without one.
958
+ let withId = (~key: string, item: JSON.t): JSON.t => {
959
+ let obj = item->JSON.Decode.object->Option.mapOr(Dict.make(), Dict.copy)
960
+ obj->Dict.set("id", JSON.Encode.string(key))
961
+ JSON.Encode.object(obj)
962
+ }
963
+ let loadFrom = async (~target: string, key: string): array<JSON.t> =>
964
+ switch Bus.getQueryDb(target) {
965
+ | Some(ops) =>
966
+ await ops.loadStream(key)
967
+ ->Stream.runCollect
968
+ ->Effect.catchAll(_ => Effect.succeed([]))
969
+ ->Effect.runPromise
970
+ | None => []
971
+ }
972
+
973
+ let lookupByIndexIn = (~target: string, ~field: string, value: string): array<JSON.t> =>
974
+ switch Bus.getQueryDbIndexLookup(target) {
975
+ | Some(lookup) => lookup(field, value)
976
+ | None =>
977
+ switch Bus.getQueryDbScan(target) {
978
+ | Some(scanAll) =>
979
+ scanAll()->Array.filter(item =>
980
+ item
981
+ ->JSON.Decode.object
982
+ ->Option.flatMap(d => d->Dict.get(field))
983
+ ->Option.flatMap(JSON.Decode.string)
984
+ ->Option.mapOr(false, v => v == value)
985
+ )
986
+ | None => []
987
+ }
988
+ }
989
+
990
+ let sourceField = (root: JSON.t, field: string): option<JSON.t> =>
991
+ root->JSON.Decode.object->Option.flatMap(d => d->Dict.get(field))
992
+
993
+ let fieldResolvers = Dict.make()
994
+
995
+ idResolverConfigs->Array.forEach((config: Reventless.ReadModel.idResolverConfig) => {
996
+ let {source: {idField, subId, resolvedField}, target} = config
997
+ let (fieldName, multi) = switch resolvedField {
998
+ | Single(f) => (f, false)
999
+ | Multi(f) => (f, true)
1000
+ }
1001
+ let targetName = target.tableName
1002
+ let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
1003
+ root,
1004
+ args,
1005
+ ctx,
1006
+ ) => {
1007
+ let key =
1008
+ root->sourceField(idField)->Option.flatMap(JSON.Decode.string)->Option.getOr("")
1009
+ let rows = if key == "" {
1010
+ []
1011
+ } else {
1012
+ switch target.idField {
1013
+ | Id => (await loadFrom(~target=targetName, key))->Array.map(withId(~key, ...))
1014
+ | Index(index) => lookupByIndexIn(~target=targetName, ~field=index, key)
1015
+ | IndexWithId(_, targetIdField) =>
1016
+ lookupByIndexIn(~target=targetName, ~field=targetIdField, key)
1017
+ }
1018
+ }
1019
+ // The target's sort key, taken from the parent row or from an argument on
1020
+ // the field — the same two sources the AppSync template reads.
1021
+ let narrowed = switch (subId, target.subIdField) {
1022
+ | (Field(sourceSortField), Some(targetSortField)) =>
1023
+ let want = root->sourceField(sourceSortField)->Option.flatMap(JSON.Decode.string)
1024
+ rows->Array.filter(item =>
1025
+ item
1026
+ ->JSON.Decode.object
1027
+ ->Option.flatMap(d => d->Dict.get(targetSortField))
1028
+ ->Option.flatMap(JSON.Decode.string) == want
1029
+ )
1030
+ | (Argument(sortArgument), Some(targetSortField)) =>
1031
+ switch args->JSON.Decode.object->Option.flatMap(d => d->Dict.get(sortArgument)) {
1032
+ | None => rows
1033
+ | Some(want) =>
1034
+ rows->Array.filter(item =>
1035
+ item
1036
+ ->JSON.Decode.object
1037
+ ->Option.flatMap(d => d->Dict.get(targetSortField))
1038
+ ->Option.flatMap(JSON.Decode.string) == want->JSON.Decode.string
1039
+ )
1040
+ }
1041
+ | _ => rows
1042
+ }
1043
+ let allowed = narrowed->Array.filter(item => targetAllows(~ctx, ~target=targetName, item))
1044
+ if multi {
1045
+ allowed->JSON.Encode.array
1046
+ } else {
1047
+ allowed->Array.get(0)->Option.getOr(JSON.Encode.null)
1048
+ }
1049
+ }
1050
+ fieldResolvers->Dict.set(fieldName, resolver)
1051
+ })
1052
+
1053
+ idsResolverConfigs->Array.forEach((config: Reventless.ReadModel.idsResolverConfig) => {
1054
+ let {source: {idsField, resolvedField}, target} = config
1055
+ let targetName = target.tableName
1056
+ let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
1057
+ root,
1058
+ _args,
1059
+ ctx,
1060
+ ) => {
1061
+ let ids =
1062
+ root
1063
+ ->sourceField(idsField)
1064
+ ->Option.flatMap(JSON.Decode.array)
1065
+ ->Option.getOr([])
1066
+ ->Array.filterMap(JSON.Decode.string)
1067
+ // Missing ids drop out rather than becoming nulls, matching BatchGetItem
1068
+ // and the by-ids door built on it.
1069
+ let rows = await ids->Array.map(async key =>
1070
+ (await loadFrom(~target=targetName, key))->Array.get(0)->Option.map(withId(~key, ...))
1071
+ )->Promise.all
1072
+ rows
1073
+ ->Array.filterMap(row =>
1074
+ row->Option.filter(item => targetAllows(~ctx, ~target=targetName, item))
1075
+ )
1076
+ ->JSON.Encode.array
1077
+ }
1078
+ fieldResolvers->Dict.set(resolvedField, resolver)
1079
+ })
1080
+
1081
+ if fieldResolvers->Dict.keysToArray->Array.length > 0 {
1082
+ server.registerFieldResolvers(~typeName=returnTypeName, ~resolvers=fieldResolvers)
1083
+ }
1084
+
916
1085
  // -- Register all fields --------------------------------------------------
917
1086
  let allSdl =
918
1087
  [byIdSdl]
@@ -8,6 +8,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
8
  import * as Effect from "effect/Effect";
9
9
  import * as Stdlib_Nullable from "@rescript/runtime/lib/es6/Stdlib_Nullable.js";
10
10
  import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
11
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
11
12
  import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
12
13
  import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
13
14
  import * as Api_Ids$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Ids.res.mjs";
@@ -54,7 +55,7 @@ function Make(Bus) {
54
55
  nodeTypeRegistry: DomainGraphQL_Server$ReventlessLocal.nodeTypeRegistry
55
56
  }
56
57
  };
57
- let make = (name, param, param$1, param$2, indexes, subIdField, param$3, param$4, authorization, param$5) => {
58
+ let make = (name, param, param$1, param$2, indexes, subIdField, idResolverConfigs, idsResolverConfigs, authorization, param$3) => {
58
59
  let server = serverRef.contents;
59
60
  let relay = relayRef.contents;
60
61
  if (relay !== undefined) {
@@ -617,6 +618,122 @@ function Make(Bus) {
617
618
  resolver
618
619
  ];
619
620
  });
621
+ let targetAllows = (ctx, target, item) => {
622
+ let schema = Plugin_Helpers$ReventlessCore.stateSchemaRegistry[target];
623
+ let cell = field => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[field]);
624
+ let identity = extractIdentity(ctx);
625
+ let match = OwnerScope$Reventless.decide(identity, Stdlib_Option.flatMap(schema, s => Owner$Reventless.fieldNames(s)[0]), undefined);
626
+ let ownerOk;
627
+ if (typeof match !== "object") {
628
+ ownerOk = match === "Unscoped";
629
+ } else {
630
+ let required = match._1;
631
+ ownerOk = Stdlib_Option.mapOr(Stdlib_Option.flatMap(cell(match._0), Stdlib_JSON.Decode.string), false, v => v === required);
632
+ }
633
+ let retiredSpec = Stdlib_Option.flatMap(Stdlib_Option.flatMap(schema, StateAnnotations$Reventless.getSpec), spec => spec.retired);
634
+ let scope = OwnerScope$Reventless.retiredScopeOf(OwnerScope$Reventless.decideRetired(identity, Stdlib_Option.map(retiredSpec, r => r.field), Stdlib_Option.flatMap(retiredSpec, r => r.values), false, undefined));
635
+ let retiredOk = scope !== undefined ? !OwnerScope$Reventless.isRetiredValue(scope, cell(scope.field)) : true;
636
+ if (ownerOk) {
637
+ return retiredOk;
638
+ } else {
639
+ return false;
640
+ }
641
+ };
642
+ let withId = (key, item) => {
643
+ let obj = Stdlib_Option.mapOr(Stdlib_JSON.Decode.object(item), {}, prim => Object.assign({}, prim));
644
+ obj["id"] = key;
645
+ return obj;
646
+ };
647
+ let loadFrom = async (target, key) => {
648
+ let ops = Bus.getQueryDb(target);
649
+ if (ops !== undefined) {
650
+ return await Effect.runPromise(Effect.catchAll(Stream.runCollect(ops.loadStream(key)), param => Effect.succeed([])));
651
+ } else {
652
+ return [];
653
+ }
654
+ };
655
+ let lookupByIndexIn = (target, field, value) => {
656
+ let lookup = Bus.getQueryDbIndexLookup(target);
657
+ if (lookup !== undefined) {
658
+ return lookup(field, value);
659
+ }
660
+ let scanAll = Bus.getQueryDbScan(target);
661
+ if (scanAll !== undefined) {
662
+ return scanAll().filter(item => Stdlib_Option.mapOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[field]), Stdlib_JSON.Decode.string), false, v => v === value));
663
+ } else {
664
+ return [];
665
+ }
666
+ };
667
+ let sourceField = (root, field) => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(root), d => d[field]);
668
+ let fieldResolvers = {};
669
+ idResolverConfigs.forEach(config => {
670
+ let target = config.target;
671
+ let match = config.source;
672
+ let resolvedField = match.resolvedField;
673
+ let subId = match.subId;
674
+ let idField = match.idField;
675
+ let match$1;
676
+ match$1 = resolvedField.TAG === "Single" ? [
677
+ resolvedField._0,
678
+ false
679
+ ] : [
680
+ resolvedField._0,
681
+ true
682
+ ];
683
+ let multi = match$1[1];
684
+ let targetName = target.tableName;
685
+ let resolver = async (root, args, ctx) => {
686
+ let key = Stdlib_Option.getOr(Stdlib_Option.flatMap(sourceField(root, idField), Stdlib_JSON.Decode.string), "");
687
+ let rows;
688
+ if (key === "") {
689
+ rows = [];
690
+ } else {
691
+ let index = target.idField;
692
+ rows = typeof index !== "object" ? (await loadFrom(targetName, key)).map(extra => withId(key, extra)) : (
693
+ index.TAG === "Index" ? lookupByIndexIn(targetName, index._0, key) : lookupByIndexIn(targetName, index._1, key)
694
+ );
695
+ }
696
+ let match = target.subIdField;
697
+ let narrowed;
698
+ if (typeof subId !== "object") {
699
+ narrowed = rows;
700
+ } else if (subId.TAG === "Field") {
701
+ if (match !== undefined) {
702
+ let want = Stdlib_Option.flatMap(sourceField(root, subId._0), Stdlib_JSON.Decode.string);
703
+ narrowed = rows.filter(item => Primitive_object.equal(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[match]), Stdlib_JSON.Decode.string), want));
704
+ } else {
705
+ narrowed = rows;
706
+ }
707
+ } else if (match !== undefined) {
708
+ let sortArgument = subId._0;
709
+ let want$1 = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d[sortArgument]);
710
+ narrowed = want$1 !== undefined ? rows.filter(item => Primitive_object.equal(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[match]), Stdlib_JSON.Decode.string), Stdlib_JSON.Decode.string(want$1))) : rows;
711
+ } else {
712
+ narrowed = rows;
713
+ }
714
+ let allowed = narrowed.filter(item => targetAllows(ctx, targetName, item));
715
+ if (multi) {
716
+ return allowed;
717
+ } else {
718
+ return Stdlib_Option.getOr(allowed[0], null);
719
+ }
720
+ };
721
+ fieldResolvers[match$1[0]] = resolver;
722
+ });
723
+ idsResolverConfigs.forEach(config => {
724
+ let match = config.source;
725
+ let idsField = match.idsField;
726
+ let targetName = config.target.tableName;
727
+ let resolver = async (root, _args, ctx) => {
728
+ let ids = Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(sourceField(root, idsField), Stdlib_JSON.Decode.array), []), Stdlib_JSON.Decode.string);
729
+ let rows = await Promise.all(ids.map(async key => Stdlib_Option.map((await loadFrom(targetName, key))[0], extra => withId(key, extra))));
730
+ return Stdlib_Array.filterMap(rows, row => Stdlib_Option.filter(row, item => targetAllows(ctx, targetName, item)));
731
+ };
732
+ fieldResolvers[match.resolvedField] = resolver;
733
+ });
734
+ if (Object.keys(fieldResolvers).length !== 0) {
735
+ server.registerFieldResolvers(returnTypeName, fieldResolvers);
736
+ }
620
737
  let allSdl = [byIdSdl].concat(byIdsSdl).concat(refsSdl).concat(match[0]).concat(itemsSdl).concat(indexSdlFields);
621
738
  let resolvers = {};
622
739
  resolvers[singleQueryName] = byIdResolver;
@@ -0,0 +1,253 @@
1
+ // Behavioural tests for the in-memory cross-table field resolvers
2
+ // (`@resolves` / `@resolvesMany`). An Orders view carries a foreign `productId`
3
+ // and a `productIds` array; both follow into a Products view's rows.
4
+ //
5
+ // The rows handed back are the TARGET's, so the target's `@retired` rule narrows
6
+ // them — the same reading the AppSync response bakes in. A nested field takes no
7
+ // `includeRetired` argument, so a retired row never travels through one.
8
+
9
+ @@warning("-44")
10
+
11
+ open JestGlobals
12
+
13
+ let _ = TestRunner.setup()
14
+
15
+ @schema
16
+ type productRow = {productId: string, name: string, archived: bool}
17
+
18
+ @schema
19
+ type orderRow = {orderId: string, productId: string, productIds: array<string>}
20
+
21
+ let annotations = (~retired: option<Reventless.StateAnnotations.retiredSpec>) => {
22
+ Reventless.StateAnnotations.ids: [],
23
+ compositeIds: [],
24
+ subIds: [],
25
+ compositeSubIds: [],
26
+ indexes: [],
27
+ hidden: [],
28
+ summary: [],
29
+ drillTargets: [],
30
+ drillTargetKeys: [],
31
+ collapsed: [],
32
+ scan: [],
33
+ scanSort: [],
34
+ semantic: [],
35
+ metric: [],
36
+ lifecycle: None,
37
+ groupBy: None,
38
+ visibility: None,
39
+ live: None,
40
+ retired,
41
+ }
42
+
43
+ let ctxFor = (~groups: array<string>): JSON.t =>
44
+ JSON.Encode.object(
45
+ Dict.fromArray([
46
+ (
47
+ "request",
48
+ JSON.Encode.object(Dict.fromArray([("headers", JSON.Encode.object(Dict.make()))])),
49
+ ),
50
+ (
51
+ "identity",
52
+ (
53
+ {
54
+ ...Reventless.Identity.anonymous,
55
+ userId: "test-user",
56
+ username: "test-user",
57
+ groups,
58
+ }: Reventless.Identity.t
59
+ )->Obj.magic,
60
+ ),
61
+ ]),
62
+ )
63
+
64
+ let plainCtx = ctxFor(~groups=["User"])
65
+
66
+ let noArgs: JSON.t = JSON.Encode.object(Dict.make())
67
+
68
+ let getString = (json: JSON.t, key: string): option<string> =>
69
+ json->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))->Option.flatMap(JSON.Decode.string)
70
+
71
+ let names = (json: JSON.t): array<string> =>
72
+ json->JSON.Decode.array->Option.getOr([])->Array.filterMap(item => item->getString("name"))
73
+
74
+ // One Bus carrying both views, resolvers registered for the Orders view with the
75
+ // two configs the PPX writes from `@resolves` / `@resolvesMany`.
76
+ let buildFixture = async (~suffix: string, ~retired: option<Reventless.StateAnnotations.retiredSpec>) => {
77
+ module Bus = LocalBus.Make()
78
+ module Storage = LocalQueryDbStorage.Make(Bus)
79
+ module Resolvers = QueryDbResolvers_GraphQL.Make(Bus)
80
+
81
+ let productsName = "Products" ++ suffix
82
+ let ordersName = "Orders" ++ suffix
83
+ let orderTypeName = "Shop_Order" ++ suffix
84
+
85
+ module ProductsSpec = {
86
+ module Id = Reventless.Id.StringPure
87
+ let name = productsName
88
+ let moduleUrl: string = %raw(`import.meta.url`)
89
+ @schema
90
+ type state = productRow
91
+ let config = Reventless.ReadModel.config()
92
+ let subIdConfig = None
93
+ let authorization: Reventless.Authorization.permission = AllowAuthenticated
94
+ let visibility: Reventless.Visibility.t = Public
95
+ }
96
+
97
+ module OrdersSpec = {
98
+ module Id = Reventless.Id.StringPure
99
+ let name = ordersName
100
+ let moduleUrl: string = %raw(`import.meta.url`)
101
+ @schema
102
+ type state = orderRow
103
+ let config = Reventless.ReadModel.config(
104
+ ~idResolvers=[
105
+ {
106
+ source: {
107
+ Reventless.ReadModel.idField: "productId",
108
+ subId: NoSubId,
109
+ resolvedField: Single("product"),
110
+ },
111
+ target: {Reventless.ReadModel.tableName: productsName, idField: Id},
112
+ },
113
+ ],
114
+ ~idsResolvers=[
115
+ {
116
+ source: {Reventless.ReadModel.idsField: "productIds", resolvedField: "products"},
117
+ target: {Reventless.ReadModel.tableName: productsName},
118
+ },
119
+ ],
120
+ )
121
+ let subIdConfig = None
122
+ let authorization: Reventless.Authorization.permission = AllowAuthenticated
123
+ let visibility: Reventless.Visibility.t = Public
124
+ }
125
+
126
+ module NoResolvers = ReventlessCore.QueryDb_Adapter.NoResolvers(Storage)
127
+ module ProductsDb = ReventlessCore.QueryDb_Builder.Make(ProductsSpec, Storage, NoResolvers)
128
+ module OrdersDb = ReventlessCore.QueryDb_Builder.Make(OrdersSpec, Storage, NoResolvers)
129
+
130
+ let register = (~viewName, ~returnTypeName) =>
131
+ ReventlessCore.Plugin_Helpers.queryFieldNamesRegistry->Dict.set(
132
+ viewName,
133
+ {
134
+ singleFieldName: returnTypeName,
135
+ listFieldName: returnTypeName ++ "s",
136
+ returnTypeName,
137
+ pluralTypeName: returnTypeName ++ "s",
138
+ includeIdParam: true,
139
+ connectionSpec: true,
140
+ },
141
+ )
142
+ register(~viewName=productsName, ~returnTypeName="Shop_Product" ++ suffix)
143
+ register(~viewName=ordersName, ~returnTypeName=orderTypeName)
144
+ ReventlessCore.Plugin_Helpers.stateSchemaRegistry->Dict.set(
145
+ productsName,
146
+ productRowSchema
147
+ ->S.Metadata.set(
148
+ ~id=Reventless.StateAnnotations.stateAnnotationsId,
149
+ annotations(~retired),
150
+ )
151
+ ->S.castToUnknown,
152
+ )
153
+
154
+ let productsDb = ProductsDb.make(~api=(), ~apiRole=())
155
+ let productOps = await productsDb->ReventlessCore.Component.operations->TestRunner.resolve
156
+ let ordersDb = OrdersDb.make(~api=(), ~apiRole=())
157
+ let orderOps = await ordersDb->ReventlessCore.Component.operations->TestRunner.resolve
158
+
159
+ let _: ReventlessCore.QueryDb_Adapter.resolvers = Resolvers.make(
160
+ ~name=ordersName,
161
+ ~api=(),
162
+ ~apiRole=(),
163
+ ~dataSourceName=""->Pulumi.Output.make,
164
+ ~indexes=[],
165
+ ~subIdField=None,
166
+ ~idResolverConfigs=OrdersSpec.config.idResolvers,
167
+ ~idsResolverConfigs=OrdersSpec.config.idsResolvers,
168
+ ~authorization=Reventless.Authorization.AllowAuthenticated,
169
+ ~opts=({}: Pulumi.CustomResourceOptions.t),
170
+ )
171
+
172
+ let _ = await productOps.save("p-1", {productId: "p-1", name: "Book", archived: false}, Init, None)
173
+ let _ = await productOps.save("p-2", {productId: "p-2", name: "Pen", archived: true}, Init, None)
174
+ let _ = await orderOps.save(
175
+ "o-1",
176
+ {orderId: "o-1", productId: "p-1", productIds: ["p-1", "gone", "p-2"]},
177
+ Init,
178
+ None,
179
+ )
180
+
181
+ let resolverFor = field =>
182
+ switch DomainGraphQL_Server.getFieldResolver(~typeName=orderTypeName, field) {
183
+ | Some(r) => r
184
+ | None => JsError.throwWithMessage("field resolver not registered: " ++ field)
185
+ }
186
+
187
+ let order = JSON.Encode.object(
188
+ Dict.fromArray([
189
+ ("orderId", JSON.Encode.string("o-1")),
190
+ ("productId", JSON.Encode.string("p-1")),
191
+ (
192
+ "productIds",
193
+ ["p-1", "gone", "p-2"]->Array.map(JSON.Encode.string)->JSON.Encode.array,
194
+ ),
195
+ ]),
196
+ )
197
+
198
+ (resolverFor, order)
199
+ }
200
+
201
+ describe("QueryDb cross-table field resolvers", () => {
202
+ beforeEach(() => {
203
+ DomainGraphQL_Server.reset()
204
+ })
205
+
206
+ testPromise("@resolves follows the foreign key into the target's row", async () => {
207
+ let (resolverFor, order) = await buildFixture(~suffix="A", ~retired=None)
208
+ let product = await resolverFor("product")(order, noArgs, plainCtx)
209
+ expect((product->getString("name"), product->getString("id")))->toEqual((
210
+ Some("Book"),
211
+ Some("p-1"),
212
+ ))
213
+ })
214
+
215
+ testPromise("@resolvesMany batch-follows the id array", async () => {
216
+ let (resolverFor, order) = await buildFixture(~suffix="B", ~retired=None)
217
+ let products = await resolverFor("products")(order, noArgs, plainCtx)
218
+ // "gone" names no row and drops out rather than becoming a null, matching
219
+ // BatchGetItem and the by-ids door built on it.
220
+ expect(products->names)->toEqual(["Book", "Pen"])
221
+ })
222
+
223
+ testPromise("a foreign key naming no row resolves to nothing", async () => {
224
+ let (resolverFor, _) = await buildFixture(~suffix="C", ~retired=None)
225
+ let orphan = JSON.Encode.object(
226
+ Dict.fromArray([("productId", JSON.Encode.string("nope"))]),
227
+ )
228
+ let product = await resolverFor("product")(orphan, noArgs, plainCtx)
229
+ expect(product)->toBe(JSON.Encode.null)
230
+ })
231
+
232
+ // The narrowing is the target's: Products declares the retirement, Orders
233
+ // declares the field, and the caller reading through Orders is answered by
234
+ // the Products rule.
235
+ testPromise("a retired target row is withheld from both forms", async () => {
236
+ let retired: Reventless.StateAnnotations.retiredSpec = {
237
+ field: "archived",
238
+ label: "",
239
+ showWhenFalse: false,
240
+ values: None,
241
+ namedWhenRetired: false,
242
+ }
243
+ let (resolverFor, order) = await buildFixture(~suffix="D", ~retired=Some(retired))
244
+ let products = await resolverFor("products")(order, noArgs, plainCtx)
245
+ expect(products->names)->toEqual(["Book"])
246
+
247
+ let archivedOnly = JSON.Encode.object(
248
+ Dict.fromArray([("productId", JSON.Encode.string("p-2"))]),
249
+ )
250
+ let product = await resolverFor("product")(archivedOnly, noArgs, plainCtx)
251
+ expect(product)->toBe(JSON.Encode.null)
252
+ })
253
+ })
@@ -0,0 +1,291 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
7
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as Pulumi from "@pulumi/pulumi";
9
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
10
+ import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
11
+ import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
12
+ import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
13
+ import * as LocalBus$ReventlessLocal from "../../src/adapter/LocalBus.res.mjs";
14
+ import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
15
+ import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
16
+ import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
17
+ import * as QueryDb_Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Adapter.res.mjs";
18
+ import * as QueryDb_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Builder.res.mjs";
19
+ import * as LocalQueryDbStorage$ReventlessLocal from "../../src/adapter/QueryDb/LocalQueryDbStorage.res.mjs";
20
+ import * as DomainGraphQL_Server$ReventlessLocal from "../../src/adapter/DomainGraphQL_Server.res.mjs";
21
+ import * as QueryDbResolvers_GraphQL$ReventlessLocal from "../../src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs";
22
+
23
+ TestRunner$ReventlessLocal.setup();
24
+
25
+ let productRowSchema = Sury.$schema(s => ({
26
+ productId: s.m(Sury.string),
27
+ name: s.m(Sury.string),
28
+ archived: s.m(Sury.bool)
29
+ }));
30
+
31
+ let orderRowSchema = Sury.$schema(s => ({
32
+ orderId: s.m(Sury.string),
33
+ productId: s.m(Sury.string),
34
+ productIds: s.m(Sury.array(Sury.string))
35
+ }));
36
+
37
+ function annotations(retired) {
38
+ return {
39
+ ids: [],
40
+ compositeIds: [],
41
+ subIds: [],
42
+ compositeSubIds: [],
43
+ indexes: [],
44
+ hidden: [],
45
+ summary: [],
46
+ drillTargets: [],
47
+ drillTargetKeys: [],
48
+ collapsed: [],
49
+ scan: [],
50
+ scanSort: [],
51
+ semantic: [],
52
+ metric: [],
53
+ lifecycle: undefined,
54
+ groupBy: undefined,
55
+ visibility: undefined,
56
+ live: undefined,
57
+ retired: retired
58
+ };
59
+ }
60
+
61
+ function ctxFor(groups) {
62
+ let newrecord = {...Identity$Reventless.anonymous};
63
+ return Object.fromEntries([
64
+ [
65
+ "request",
66
+ Object.fromEntries([[
67
+ "headers",
68
+ {}
69
+ ]])
70
+ ],
71
+ [
72
+ "identity",
73
+ (newrecord.groups = groups, newrecord.username = "test-user", newrecord.userId = "test-user", newrecord)
74
+ ]
75
+ ]);
76
+ }
77
+
78
+ let plainCtx = ctxFor(["User"]);
79
+
80
+ let noArgs = {};
81
+
82
+ function getString(json, key) {
83
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d[key]), Stdlib_JSON.Decode.string);
84
+ }
85
+
86
+ function names(json) {
87
+ return Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_JSON.Decode.array(json), []), item => getString(item, "name"));
88
+ }
89
+
90
+ async function buildFixture(suffix, retired) {
91
+ let Bus = LocalBus$ReventlessLocal.Make({});
92
+ let Storage = LocalQueryDbStorage$ReventlessLocal.Make(Bus);
93
+ let Resolvers = QueryDbResolvers_GraphQL$ReventlessLocal.Make(Bus);
94
+ let productsName = "Products" + suffix;
95
+ let ordersName = "Orders" + suffix;
96
+ let orderTypeName = "Shop_Order" + suffix;
97
+ let moduleUrl = import.meta.url;
98
+ let config = ReadModel$Reventless.config(undefined, undefined, undefined);
99
+ let moduleUrl$1 = import.meta.url;
100
+ let config$1 = ReadModel$Reventless.config([{
101
+ source: {
102
+ idField: "productId",
103
+ subId: "NoSubId",
104
+ resolvedField: {
105
+ TAG: "Single",
106
+ _0: "product"
107
+ }
108
+ },
109
+ target: {
110
+ tableName: productsName,
111
+ idField: "Id"
112
+ }
113
+ }], [{
114
+ source: {
115
+ idsField: "productIds",
116
+ resolvedField: "products"
117
+ },
118
+ target: {
119
+ tableName: productsName
120
+ }
121
+ }], undefined);
122
+ let NoResolvers = QueryDb_Adapter$ReventlessCore.NoResolvers({
123
+ make: Storage.make
124
+ });
125
+ let ProductsDb = QueryDb_Builder$ReventlessCore.Make({
126
+ Id: {
127
+ schema: Id$Reventless.StringPure.schema,
128
+ make: prim => prim,
129
+ makeFromString: prim => prim,
130
+ toString: prim => prim,
131
+ cmp: Id$Reventless.StringPure.cmp
132
+ },
133
+ name: productsName,
134
+ moduleUrl: moduleUrl,
135
+ stateSchema: productRowSchema,
136
+ config: config,
137
+ subIdConfig: undefined,
138
+ authorization: "AllowAuthenticated",
139
+ visibility: "Public"
140
+ })({
141
+ make: Storage.make
142
+ })(NoResolvers);
143
+ let OrdersDb = QueryDb_Builder$ReventlessCore.Make({
144
+ Id: {
145
+ schema: Id$Reventless.StringPure.schema,
146
+ make: prim => prim,
147
+ makeFromString: prim => prim,
148
+ toString: prim => prim,
149
+ cmp: Id$Reventless.StringPure.cmp
150
+ },
151
+ name: ordersName,
152
+ moduleUrl: moduleUrl$1,
153
+ stateSchema: orderRowSchema,
154
+ config: config$1,
155
+ subIdConfig: undefined,
156
+ authorization: "AllowAuthenticated",
157
+ visibility: "Public"
158
+ })({
159
+ make: Storage.make
160
+ })(NoResolvers);
161
+ let register = (viewName, returnTypeName) => {
162
+ Plugin_Helpers$ReventlessCore.queryFieldNamesRegistry[viewName] = {
163
+ singleFieldName: returnTypeName,
164
+ listFieldName: returnTypeName + "s",
165
+ returnTypeName: returnTypeName,
166
+ pluralTypeName: returnTypeName + "s",
167
+ includeIdParam: true,
168
+ connectionSpec: true
169
+ };
170
+ };
171
+ register(productsName, "Shop_Product" + suffix);
172
+ register(ordersName, orderTypeName);
173
+ Plugin_Helpers$ReventlessCore.stateSchemaRegistry[productsName] = Sury.$Metadata_set(productRowSchema, StateAnnotations$Reventless.stateAnnotationsId, annotations(retired));
174
+ let productsDb = ProductsDb.make(undefined, undefined, undefined, undefined, undefined);
175
+ let productOps = await TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(productsDb));
176
+ let ordersDb = OrdersDb.make(undefined, undefined, undefined, undefined, undefined);
177
+ let orderOps = await TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(ordersDb));
178
+ Resolvers.make(ordersName, undefined, undefined, Pulumi.output(""), [], undefined, config$1.idResolvers, config$1.idsResolvers, "AllowAuthenticated", {});
179
+ await productOps.save("p-1", {
180
+ productId: "p-1",
181
+ name: "Book",
182
+ archived: false
183
+ }, "Init", undefined);
184
+ await productOps.save("p-2", {
185
+ productId: "p-2",
186
+ name: "Pen",
187
+ archived: true
188
+ }, "Init", undefined);
189
+ await orderOps.save("o-1", {
190
+ orderId: "o-1",
191
+ productId: "p-1",
192
+ productIds: [
193
+ "p-1",
194
+ "gone",
195
+ "p-2"
196
+ ]
197
+ }, "Init", undefined);
198
+ let resolverFor = field => {
199
+ let r = DomainGraphQL_Server$ReventlessLocal.getFieldResolver(orderTypeName, field);
200
+ if (r !== undefined) {
201
+ return r;
202
+ } else {
203
+ return Stdlib_JsError.throwWithMessage("field resolver not registered: " + field);
204
+ }
205
+ };
206
+ let order = Object.fromEntries([
207
+ [
208
+ "orderId",
209
+ "o-1"
210
+ ],
211
+ [
212
+ "productId",
213
+ "p-1"
214
+ ],
215
+ [
216
+ "productIds",
217
+ [
218
+ "p-1",
219
+ "gone",
220
+ "p-2"
221
+ ].map(prim => prim)
222
+ ]
223
+ ]);
224
+ return [
225
+ resolverFor,
226
+ order
227
+ ];
228
+ }
229
+
230
+ globalThis.describe("QueryDb cross-table field resolvers", () => {
231
+ globalThis.beforeEach(() => DomainGraphQL_Server$ReventlessLocal.reset());
232
+ globalThis.test("@resolves follows the foreign key into the target's row", async () => {
233
+ let match = await buildFixture("A", undefined);
234
+ let product = await match[0]("product")(match[1], noArgs, plainCtx);
235
+ globalThis.expect([
236
+ getString(product, "name"),
237
+ getString(product, "id")
238
+ ]).toEqual([
239
+ "Book",
240
+ "p-1"
241
+ ]);
242
+ });
243
+ globalThis.test("@resolvesMany batch-follows the id array", async () => {
244
+ let match = await buildFixture("B", undefined);
245
+ let products = await match[0]("products")(match[1], noArgs, plainCtx);
246
+ globalThis.expect(names(products)).toEqual([
247
+ "Book",
248
+ "Pen"
249
+ ]);
250
+ });
251
+ globalThis.test("a foreign key naming no row resolves to nothing", async () => {
252
+ let match = await buildFixture("C", undefined);
253
+ let orphan = Object.fromEntries([[
254
+ "productId",
255
+ "nope"
256
+ ]]);
257
+ let product = await match[0]("product")(orphan, noArgs, plainCtx);
258
+ globalThis.expect(product).toBe(null);
259
+ });
260
+ globalThis.test("a retired target row is withheld from both forms", async () => {
261
+ let match = await buildFixture("D", {
262
+ field: "archived",
263
+ label: "",
264
+ showWhenFalse: false,
265
+ values: undefined,
266
+ namedWhenRetired: false
267
+ });
268
+ let resolverFor = match[0];
269
+ let products = await resolverFor("products")(match[1], noArgs, plainCtx);
270
+ globalThis.expect(names(products)).toEqual(["Book"]);
271
+ let archivedOnly = Object.fromEntries([[
272
+ "productId",
273
+ "p-2"
274
+ ]]);
275
+ let product = await resolverFor("product")(archivedOnly, noArgs, plainCtx);
276
+ globalThis.expect(product).toBe(null);
277
+ });
278
+ });
279
+
280
+ export {
281
+ productRowSchema,
282
+ orderRowSchema,
283
+ annotations,
284
+ ctxFor,
285
+ plainCtx,
286
+ noArgs,
287
+ getString,
288
+ names,
289
+ buildFixture,
290
+ }
291
+ /* Not a pure module */