@reventlessdev/reventless-aws 3.0.0-alpha.317 → 3.0.0-alpha.319

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,29 @@
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.319 (2026-08-22)
7
+
8
+ * feat(core)!: a slice that has given up says so ([f5559ab](https://github.com/ReventlessDev/reventless-core/commit/f5559abede3a9cf2c9a48aadc69a666c46cc75bf))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * `onExhausted` is required on the Translation and Automation
13
+ module types. ReScript has no optional module-type fields, so the alternative
14
+ was a PPX-injected default — and defaulting this one to silence is the bug.
15
+ A slice now states what abandonment means for its domain, `None` included.
16
+ GeocodeCustomerAddress answers with MarkAddressUnresolvable; the others say
17
+ nothing, on purpose.
18
+
19
+
20
+
21
+ # 3.0.0-alpha.318 (2026-08-21)
22
+
23
+ ### Bug Fixes
24
+
25
+ * **core:** make @resolves/[@resolves](https://github.com/resolves)Many work end to end ([4c52957](https://github.com/ReventlessDev/reventless-core/commit/4c5295759dcb4b3eda4f0f4f2c1bc387fed88fcb))
26
+ * **deps:** update sury to 11.0.0-rc.2 to fix unreachable union constructors ([fa5744f](https://github.com/ReventlessDev/reventless-core/commit/fa5744fed8de975e2f14725c856c6e5ce7d04a74))
27
+
28
+
6
29
  # 3.0.0-alpha.317 (2026-08-20)
7
30
 
8
31
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.317",
3
+ "version": "3.0.0-alpha.319",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -10,24 +10,24 @@
10
10
  "@aws-sdk/client-location": "3.970.0",
11
11
  "@aws-sdk/client-s3": "3.970.0",
12
12
  "@aws-sdk/s3-request-presigner": "3.970.0",
13
- "sury": "11.0.0-rc.1",
13
+ "sury": "11.0.0-rc.2",
14
14
  "uuid": "^13.0.0",
15
15
  "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.12",
16
16
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
17
- "@reventlessdev/rescript-node": "2.0.0-alpha.8",
18
17
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
19
- "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.1",
18
+ "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.2",
19
+ "@reventlessdev/rescript-node": "2.0.0-alpha.8",
20
20
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
21
+ "@reventlessdev/reventless-core": "3.0.0-alpha.247",
21
22
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
- "@reventlessdev/reventless-core": "3.0.0-alpha.245",
23
- "@reventlessdev/reventless-infra": "3.0.0-alpha.148",
24
- "@reventlessdev/reventless-interop": "3.0.0-alpha.32",
25
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.109",
26
- "@reventlessdev/reventless-spec": "3.0.0-alpha.120"
23
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.150",
24
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.33",
25
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.122",
26
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.111"
27
27
  },
28
28
  "devDependencies": {
29
29
  "rescript": "12.3.0",
30
- "sury-ppx": "11.0.0-rc.1"
30
+ "sury-ppx": "11.0.0-rc.2"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "rescript": "12.3.0"
@@ -443,10 +443,15 @@ let dispatch = async (
443
443
  items->Array.filter(it => it->argStr(subField)->Option.getOr("") == subVal)
444
444
  | _ => items
445
445
  }
446
+ // The target's own rules, applied to the target's rows — `binding` here is
447
+ // the target's, so `ownerAllows` / `retiredAllows` ask its questions. A
448
+ // nested field takes no `includeRetired` argument, so a retired row never
449
+ // travels through one.
450
+ let allowed = filtered->Array.filter(item => ownerAllows(item) && retiredAllows(item))
446
451
  if payload.multi->Option.getOr(false) {
447
- JSON.Encode.array(filtered)
452
+ JSON.Encode.array(allowed)
448
453
  } else {
449
- switch filtered->Array.get(0) {
454
+ switch allowed->Array.get(0) {
450
455
  | Some(item) => item
451
456
  | None => JSON.Encode.null
452
457
  }
@@ -459,7 +464,11 @@ let dispatch = async (
459
464
  let target = payload.target->Option.getOr(rm)
460
465
  let source = payload.source->Option.getOr(JSON.Encode.null)
461
466
  let ids = source->argStrs(payload.sourceIdsField->Option.getOr(""))
462
- JSON.Encode.array(await binding.pushdowns.byIds(~readModelName=target, ids))
467
+ JSON.Encode.array(
468
+ (await binding.pushdowns.byIds(~readModelName=target, ids))->Array.filter(item =>
469
+ ownerAllows(item) && retiredAllows(item)
470
+ ),
471
+ )
463
472
 
464
473
  | other =>
465
474
  // Fail loudly on unmapped kinds (feature-parity guard, per the B3.2 plan).
@@ -241,7 +241,13 @@ async function dispatch(binding, lookupBindingOpt, payload) {
241
241
  let target = Stdlib_Option.getOr(payload.target, rm);
242
242
  let source = Stdlib_Option.getOr(payload.source, null);
243
243
  let ids$2 = argStrs(source, Stdlib_Option.getOr(payload.sourceIdsField, ""));
244
- return await binding.pushdowns.byIds(target, ids$2);
244
+ return (await binding.pushdowns.byIds(target, ids$2)).filter(item => {
245
+ if (ownerAllows(item)) {
246
+ return retiredAllows(item);
247
+ } else {
248
+ return false;
249
+ }
250
+ });
245
251
  case "resolveOne" :
246
252
  let target$1 = Stdlib_Option.getOr(payload.target, rm);
247
253
  let source$1 = Stdlib_Option.getOr(payload.source, null);
@@ -265,10 +271,17 @@ async function dispatch(binding, lookupBindingOpt, payload) {
265
271
  } else {
266
272
  filtered = items$2;
267
273
  }
274
+ let allowed = filtered.filter(item => {
275
+ if (ownerAllows(item)) {
276
+ return retiredAllows(item);
277
+ } else {
278
+ return false;
279
+ }
280
+ });
268
281
  if (Stdlib_Option.getOr(payload.multi, false)) {
269
- return filtered;
282
+ return allowed;
270
283
  }
271
- let item = filtered[0];
284
+ let item = allowed[0];
272
285
  if (item !== undefined) {
273
286
  return item;
274
287
  } else {
@@ -522,6 +522,32 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
522
522
  None
523
523
  }
524
524
 
525
+ // The GraphQL type the cross-table fields hang off. `name` is the QueryDb's
526
+ // own (capitalised spec) name; the SDL calls the type by its plugin-prefixed
527
+ // name, and a resolver naming the other one is refused at deploy with
528
+ // "No field named X found on type Y".
529
+ let parentTypeName = switch registryEntry {
530
+ | Some({returnTypeName}) => returnTypeName
531
+ | None => name
532
+ }
533
+
534
+ // A cross-table field hands back rows from the TARGET's table, so the owner
535
+ // and retirement rules that apply are the target's. Read from the same
536
+ // registry this view reads its own from — populated for every queryable in
537
+ // the plugin before any resolver is built.
538
+ let targetScope = (targetName: string) => {
539
+ let schema = ReventlessCore.Plugin_Helpers.stateSchemaRegistry->Dict.get(targetName)
540
+ let retired =
541
+ schema
542
+ ->Option.flatMap(Reventless.StateAnnotations.getSpec)
543
+ ->Option.flatMap(spec => spec.retired)
544
+ (
545
+ schema->Option.flatMap(s => Reventless.Owner.fieldNames(s)->Array.get(0)),
546
+ retired->Option.map(r => r.field),
547
+ retired->Option.flatMap(r => r.values),
548
+ )
549
+ }
550
+
525
551
  let idResolvers = idResolverConfigs->Array.map(config => {
526
552
  let {
527
553
  source: {idField: sourceIdField, subId: sourceSubId, resolvedField},
@@ -533,71 +559,100 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
533
559
  | _ => ("", "")
534
560
  }
535
561
  let storageResource = storageResource(~pluginName=target.pluginName, ~tableName)
536
- switch resolvedField {
537
- | Single(field)
538
- | Multi(field) =>
539
- let dataSourceName =
540
- DataSource.makeDynamoDBDataSourceWithTableName(
541
- ~name=name ++ (field->String.capitalize ++ "Resolver"),
542
- ~api,
543
- ~tableName=storageResource.name,
544
- ~serviceRole=apiRole,
545
- ~opts,
546
- ).name->Pulumi.Output.asInput
547
-
548
- Resolver.makeUnitJsResolver(
549
- ~name=name ++ field->String.capitalize,
562
+ let (targetOwnerField, targetRetiredField, targetRetiredValues) = targetScope(tableName)
563
+ let (field, multi) = switch resolvedField {
564
+ | Single(field) => (field, false)
565
+ | Multi(field) => (field, true)
566
+ }
567
+ let response = Resolver.Functions.resolvedFieldResponse(
568
+ ~multi,
569
+ ~ownerField=targetOwnerField,
570
+ ~elevatedGroups,
571
+ ~retiredField=?targetRetiredField,
572
+ ~retiredValues=?targetRetiredValues,
573
+ )
574
+ let dataSourceName =
575
+ DataSource.makeDynamoDBDataSourceWithTableName(
576
+ ~name=name ++ (field->String.capitalize ++ "Resolver"),
550
577
  ~api,
551
- ~dataSourceName,
552
- ~type_=name->Pulumi.Input.make,
553
- ~field=field->Pulumi.Input.make,
554
- ~code=switch (targetId, sourceSubId, target.subIdField) {
555
- | (Id, Field(sourceSortField), Some(targetSortField)) =>
556
- Resolver.Functions.resolveIdSort(~sourceIdField, ~sourceSortField, ~targetSortField)
557
- | (Id, Argument(sourceSortArgument), Some(targetSortField)) =>
558
- Resolver.Functions.resolveIdSortArgument(
559
- ~sourceIdField,
560
- ~sourceSortArgument,
561
- ~targetSortField,
562
- )
563
- | (Id, _, _) => Resolver.Functions.resolveId(~sourceIdField)
564
-
565
- | (_, Field(sourceSortField), Some(targetSortField)) =>
566
- Resolver.Functions.resolveIdByIndexSort(
567
- ~index,
568
- ~sourceIdField,
569
- ~targetIdField,
570
- ~sourceSortField,
571
- ~targetSortField,
572
- )
573
- | (_, Argument(sourceSortArgument), Some(targetSortField)) =>
574
- Resolver.Functions.resolveIdByIndexSortArgument(
575
- ~index,
576
- ~sourceIdField,
577
- ~targetIdField,
578
- ~sourceSortArgument,
579
- ~targetSortField,
580
- )
581
- | _ => Resolver.Functions.resolveIdByIndex(~index, ~sourceIdField, ~targetIdField)
582
- },
578
+ ~tableName=storageResource.name,
579
+ ~serviceRole=apiRole,
583
580
  ~opts,
584
- )
585
- }
581
+ ).name->Pulumi.Output.asInput
582
+
583
+ Resolver.makeUnitJsResolver(
584
+ ~name=name ++ field->String.capitalize,
585
+ ~api,
586
+ ~dataSourceName,
587
+ ~type_=parentTypeName->Pulumi.Input.make,
588
+ ~field=field->Pulumi.Input.make,
589
+ ~code=switch (targetId, sourceSubId, target.subIdField) {
590
+ | (Id, Field(sourceSortField), Some(targetSortField)) =>
591
+ Resolver.Functions.resolveIdSort(
592
+ ~sourceIdField,
593
+ ~sourceSortField,
594
+ ~targetSortField,
595
+ ~response,
596
+ )
597
+ | (Id, Argument(sourceSortArgument), Some(targetSortField)) =>
598
+ Resolver.Functions.resolveIdSortArgument(
599
+ ~sourceIdField,
600
+ ~sourceSortArgument,
601
+ ~targetSortField,
602
+ ~response,
603
+ )
604
+ | (Id, _, _) => Resolver.Functions.resolveId(~sourceIdField, ~response)
605
+
606
+ | (_, Field(sourceSortField), Some(targetSortField)) =>
607
+ Resolver.Functions.resolveIdByIndexSort(
608
+ ~index,
609
+ ~sourceIdField,
610
+ ~targetIdField,
611
+ ~sourceSortField,
612
+ ~targetSortField,
613
+ ~response,
614
+ )
615
+ | (_, Argument(sourceSortArgument), Some(targetSortField)) =>
616
+ Resolver.Functions.resolveIdByIndexSortArgument(
617
+ ~index,
618
+ ~sourceIdField,
619
+ ~targetIdField,
620
+ ~sourceSortArgument,
621
+ ~targetSortField,
622
+ ~response,
623
+ )
624
+ | _ =>
625
+ Resolver.Functions.resolveIdByIndex(~index, ~sourceIdField, ~targetIdField, ~response)
626
+ },
627
+ ~opts,
628
+ )
586
629
  })
587
630
 
631
+ // `@resolvesMany` — BatchGetItem against the target's table. Runs on THIS
632
+ // view's data source rather than one of the target's: the tables map names
633
+ // the target explicitly, and every QueryDb in the plugin has already granted
634
+ // `dynamodb:*` on itself to the API role the data source assumes.
588
635
  let idsResolvers = idsResolverConfigs->Array.map(config => {
589
636
  let {source: {idsField, resolvedField}, target: {tableName} as target} = config
590
637
  let storageResource = storageResource(~pluginName=target.pluginName, ~tableName)
638
+ let (targetOwnerField, targetRetiredField, targetRetiredValues) = targetScope(tableName)
591
639
 
592
640
  Resolver.makeUnitJsResolver(
593
- ~name=name ++ idsField->String.capitalize,
641
+ ~name=name ++ resolvedField->String.capitalize,
594
642
  ~api,
595
643
  ~dataSourceName,
596
- ~type_=name->Pulumi.Input.make,
644
+ ~type_=parentTypeName->Pulumi.Input.make,
597
645
  ~field=resolvedField->Pulumi.Input.make,
598
646
  ~code=generateCode(
599
647
  ~storageResource,
600
- ~template=Resolver.Functions.resolveIds(~idsField, ~sortField=target.subIdField, ...),
648
+ ~template=Resolver.Functions.resolveIds(
649
+ ~idsField,
650
+ ~sortField=target.subIdField,
651
+ ~ownerField=?targetOwnerField,
652
+ ~retiredField=?targetRetiredField,
653
+ ~retiredValues=?targetRetiredValues,
654
+ ~elevatedGroups,
655
+ ),
601
656
  ),
602
657
  ~opts,
603
658
  )
@@ -171,10 +171,22 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
171
171
  } else {
172
172
  resolverRefs = undefined;
173
173
  }
174
+ let parentTypeName = registryEntry !== undefined ? registryEntry.returnTypeName : name$1;
175
+ let targetScope = targetName => {
176
+ let schema = Plugin_Helpers$ReventlessCore.stateSchemaRegistry[targetName];
177
+ let retired = Stdlib_Option.flatMap(Stdlib_Option.flatMap(schema, StateAnnotations$Reventless.getSpec), spec => spec.retired);
178
+ return [
179
+ Stdlib_Option.flatMap(schema, s => Owner$Reventless.fieldNames(s)[0]),
180
+ Stdlib_Option.map(retired, r => r.field),
181
+ Stdlib_Option.flatMap(retired, r => r.values)
182
+ ];
183
+ };
174
184
  let idResolvers = idResolverConfigs.map(config => {
175
185
  let target = config.target;
176
186
  let targetId = target.idField;
187
+ let tableName = target.tableName;
177
188
  let match = config.source;
189
+ let resolvedField = match.resolvedField;
178
190
  let sourceSubId = match.subId;
179
191
  let sourceIdField = match.idField;
180
192
  let match$1;
@@ -197,40 +209,62 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
197
209
  }
198
210
  let targetIdField = match$1[1];
199
211
  let index$1 = match$1[0];
200
- let storageResource$1 = storageResource(target.pluginName, target.tableName);
201
- let field = match.resolvedField._0;
212
+ let storageResource$1 = storageResource(target.pluginName, tableName);
213
+ let match$2 = targetScope(tableName);
214
+ let match$3;
215
+ match$3 = resolvedField.TAG === "Single" ? [
216
+ resolvedField._0,
217
+ false
218
+ ] : [
219
+ resolvedField._0,
220
+ true
221
+ ];
222
+ let field = match$3[0];
223
+ let response = AppSync_Resolver_Functions$PulumiAws.resolvedFieldResponse(match$3[1], match$2[0], elevatedGroups, match$2[1], match$2[2]);
202
224
  let dataSourceName = AppSync_DataSource$PulumiAws.makeDynamoDBDataSourceWithTableName(name$1 + (Stdlib_String.capitalize(field) + "Resolver"), api, storageResource$1.name, apiRole, opts).name;
203
- let match$2 = target.subIdField;
225
+ let match$4 = target.subIdField;
204
226
  let tmp;
205
227
  let exit = 0;
228
+ let exit$1 = 0;
206
229
  if (typeof targetId !== "object") {
207
- tmp = typeof sourceSubId !== "object" ? AppSync_Resolver_Functions$PulumiAws.resolveId(sourceIdField) : (
230
+ tmp = typeof sourceSubId !== "object" ? AppSync_Resolver_Functions$PulumiAws.resolveId(sourceIdField, response) : (
208
231
  sourceSubId.TAG === "Field" ? (
209
- match$2 !== undefined ? AppSync_Resolver_Functions$PulumiAws.resolveIdSort(sourceIdField, sourceSubId._0, match$2) : AppSync_Resolver_Functions$PulumiAws.resolveId(sourceIdField)
232
+ match$4 !== undefined ? AppSync_Resolver_Functions$PulumiAws.resolveIdSort(sourceIdField, sourceSubId._0, match$4, response) : AppSync_Resolver_Functions$PulumiAws.resolveId(sourceIdField, response)
210
233
  ) : (
211
- match$2 !== undefined ? AppSync_Resolver_Functions$PulumiAws.resolveIdSortArgument(sourceIdField, sourceSubId._0, match$2) : AppSync_Resolver_Functions$PulumiAws.resolveId(sourceIdField)
234
+ match$4 !== undefined ? AppSync_Resolver_Functions$PulumiAws.resolveIdSortArgument(sourceIdField, sourceSubId._0, match$4, response) : AppSync_Resolver_Functions$PulumiAws.resolveId(sourceIdField, response)
212
235
  )
213
236
  );
214
237
  } else {
215
- exit = 1;
238
+ exit$1 = 2;
239
+ }
240
+ if (exit$1 === 2) {
241
+ if (typeof sourceSubId !== "object") {
242
+ exit = 1;
243
+ } else if (sourceSubId.TAG === "Field") {
244
+ if (match$4 !== undefined) {
245
+ tmp = AppSync_Resolver_Functions$PulumiAws.resolveIdByIndexSort(index$1, sourceIdField, sourceSubId._0, targetIdField, match$4, response);
246
+ } else {
247
+ exit = 1;
248
+ }
249
+ } else if (match$4 !== undefined) {
250
+ tmp = AppSync_Resolver_Functions$PulumiAws.resolveIdByIndexSortArgument(index$1, sourceIdField, sourceSubId._0, targetIdField, match$4, response);
251
+ } else {
252
+ exit = 1;
253
+ }
216
254
  }
217
255
  if (exit === 1) {
218
- tmp = typeof sourceSubId !== "object" ? AppSync_Resolver_Functions$PulumiAws.resolveIdByIndex(index$1, sourceIdField, targetIdField) : (
219
- sourceSubId.TAG === "Field" ? (
220
- match$2 !== undefined ? AppSync_Resolver_Functions$PulumiAws.resolveIdByIndexSort(index$1, sourceIdField, sourceSubId._0, targetIdField, match$2) : AppSync_Resolver_Functions$PulumiAws.resolveIdByIndex(index$1, sourceIdField, targetIdField)
221
- ) : (
222
- match$2 !== undefined ? AppSync_Resolver_Functions$PulumiAws.resolveIdByIndexSortArgument(index$1, sourceIdField, sourceSubId._0, targetIdField, match$2) : AppSync_Resolver_Functions$PulumiAws.resolveIdByIndex(index$1, sourceIdField, targetIdField)
223
- )
224
- );
256
+ tmp = AppSync_Resolver_Functions$PulumiAws.resolveIdByIndex(index$1, sourceIdField, targetIdField, response);
225
257
  }
226
- return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(field), api, dataSourceName, name$1, field, tmp, opts);
258
+ return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(field), api, dataSourceName, parentTypeName, field, tmp, opts);
227
259
  });
228
260
  let idsResolvers = idsResolverConfigs.map(config => {
229
261
  let target = config.target;
262
+ let tableName = target.tableName;
230
263
  let match = config.source;
231
- let idsField = match.idsField;
232
- let storageResource$1 = storageResource(target.pluginName, target.tableName);
233
- return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(idsField), api, dataSourceName, name$1, match.resolvedField, generateCode(storageResource$1, none => AppSync_Resolver_Functions$PulumiAws.resolveIds(none, idsField, target.subIdField)), opts);
264
+ let resolvedField = match.resolvedField;
265
+ let storageResource$1 = storageResource(target.pluginName, tableName);
266
+ let match$1 = targetScope(tableName);
267
+ return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(resolvedField), api, dataSourceName, parentTypeName, resolvedField, generateCode(storageResource$1, AppSync_Resolver_Functions$PulumiAws.resolveIds(match.idsField, target.subIdField, match$1[0], match$1[1], match$1[2], elevatedGroups)), opts);
234
268
  });
235
269
  let mainResolvers = resolverByIdMultiple !== undefined ? (
236
270
  resolverByIds !== undefined ? [
@@ -231,6 +231,14 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
231
231
  }
232
232
  })
233
233
 
234
+ // The GraphQL type the cross-table fields hang off — the plugin-prefixed SDL
235
+ // name, not the QueryDb's own. A resolver naming the other one is refused at
236
+ // deploy with "No field named X found on type Y".
237
+ let parentTypeName = switch registryEntry {
238
+ | Some({returnTypeName}) => returnTypeName
239
+ | None => name
240
+ }
241
+
234
242
  // @resolves — single cross-table field resolver on this (parent) type. The
235
243
  // target's binding key is its capitalized spec name (mirrors how the binding
236
244
  // registry is keyed); the parent object flows via ctx.source.
@@ -259,7 +267,7 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
259
267
  ~name=name ++ field->String.capitalize,
260
268
  ~api,
261
269
  ~dataSourceName,
262
- ~type_=name->Pulumi.Input.make,
270
+ ~type_=parentTypeName->Pulumi.Input.make,
263
271
  ~field=field->Pulumi.Input.make,
264
272
  ~code=invokeFieldTemplate(~sourceType=name, ~kind="resolveOne", ~target=targetKey, ~extra),
265
273
  ~opts,
@@ -275,7 +283,7 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
275
283
  ~name=name ++ resolvedField->String.capitalize,
276
284
  ~api,
277
285
  ~dataSourceName,
278
- ~type_=name->Pulumi.Input.make,
286
+ ~type_=parentTypeName->Pulumi.Input.make,
279
287
  ~field=resolvedField->Pulumi.Input.make,
280
288
  ~code=invokeFieldTemplate(~sourceType=name, ~kind="resolveMany", ~target=targetKey, ~extra),
281
289
  ~opts,
@@ -107,6 +107,7 @@ function make(name, api, param, dataSourceName, indexes, subIdField, idResolverC
107
107
  return mkResolver(resolverName, field, "index", index, undefined, undefined);
108
108
  }
109
109
  });
110
+ let parentTypeName = registryEntry !== undefined ? registryEntry.returnTypeName : name$1;
110
111
  let idResolvers = idResolverConfigs.map(config => {
111
112
  let target = config.target;
112
113
  let match = config.source;
@@ -139,14 +140,14 @@ function make(name, api, param, dataSourceName, indexes, subIdField, idResolverC
139
140
  let extra = `\n sourceIdField: '` + match.idField + `',\n multi: ` + (
140
141
  match$1[1] ? "true" : "false"
141
142
  ) + `,` + targetFrag + subIdFrag;
142
- return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(field), api, dataSourceName, name$1, field, invokeFieldTemplate(name$1, "resolveOne", targetKey, extra), opts);
143
+ return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(field), api, dataSourceName, parentTypeName, field, invokeFieldTemplate(name$1, "resolveOne", targetKey, extra), opts);
143
144
  });
144
145
  let idsResolvers = idsResolverConfigs.map(config => {
145
146
  let match = config.source;
146
147
  let resolvedField = match.resolvedField;
147
148
  let targetKey = Stdlib_String.capitalize(config.target.tableName);
148
149
  let extra = `\n sourceIdsField: '` + match.idsField + `',`;
149
- return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(resolvedField), api, dataSourceName, name$1, resolvedField, invokeFieldTemplate(name$1, "resolveMany", targetKey, extra), opts);
150
+ return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(resolvedField), api, dataSourceName, parentTypeName, resolvedField, invokeFieldTemplate(name$1, "resolveMany", targetKey, extra), opts);
150
151
  });
151
152
  return [
152
153
  byId,
@@ -148,9 +148,12 @@ let makeSyncTodoItems = (
148
148
  // by definition no fresher.
149
149
  //
150
150
  // Only `Pending` and `Failed` are read. `Completed` rows are the bulk of a
151
- // mature table and are never actionable. Retry-exhausted rows are not excluded
152
- // `maxRetries` is Spec-level and not known here but `phase2` filters them, so
153
- // they are inert rather than wrong.
151
+ // mature table and are never actionable, and neither are `Abandoned` ones a
152
+ // row whose retry budget is spent is terminal, so it is left in the table rather
153
+ // than carried into memory. That the status says so is what lets this filter
154
+ // decide it: `maxRetries` is Spec-level and still not known here, and before the
155
+ // status existed an exhausted row had to be loaded and then filtered out by
156
+ // `phase2`.
154
157
  let makeLoadTodoItems = (
155
158
  ~queryDbTableName: string,
156
159
  ~todoItems: dict<'row>,
@@ -308,3 +308,81 @@ describe("the unowned stub is callable where the door calls it", () => {
308
308
  ))->Expect.toEqual((true, true))
309
309
  })
310
310
  })
311
+
312
+ // ── The cross-table doors (`@resolves` / `@resolvesMany`) ────────────────────
313
+ //
314
+ // A field that follows a foreign key hands back the TARGET's rows, so the
315
+ // target's rules narrow them. The field takes no `includeRetired` argument, so
316
+ // `_wantsRetired` is never true through one — `{list}Refs` is the door that
317
+ // names a row the archive took.
318
+
319
+ describe("the cross-table batch door", () => {
320
+ let code = F.resolveIds(
321
+ ~idsField="productIds",
322
+ ~sortField=None,
323
+ ~retiredField="shelfStatus",
324
+ ~retiredValues=["Archived"],
325
+ ~elevatedGroups=elevated,
326
+ )("ProductsTable")
327
+
328
+ // BatchGetItem answers `{data: {<table>: [...]}}`; returning `ctx.result`
329
+ // handed the whole envelope to a field typed as a list, which fails at
330
+ // execution for every caller that selects it.
331
+ testSync("returns the rows out of the batch envelope", () =>
332
+ expect(code->String.includes("ctx.result?.data?.['ProductsTable']"))->Expect.toBe(true)
333
+ )
334
+
335
+ testSync("drops retired rows from the returned array", () =>
336
+ expect(code->String.includes("item !== null && _live(item)"))->Expect.toBe(true)
337
+ )
338
+
339
+ // An empty id array used to fall through to a GetItem on the PARENT's own id
340
+ // against the target's table — a read that answers a shape the field cannot be.
341
+ testSync("short-circuits an empty id array instead of reading", () =>
342
+ expect((
343
+ code->String.includes("if (idList.length === 0) return runtime.earlyReturn([]);"),
344
+ code->String.includes("operation: 'GetItem'"),
345
+ ))->toEqual((true, false))
346
+ )
347
+
348
+ testSync("is unchanged for a target that declares neither rule", () => {
349
+ let plain = F.resolveIds(~idsField="productIds", ~sortField=None)("ProductsTable")
350
+ expect((plain->String.includes("_owns"), plain->String.includes("_live")))->toEqual((
351
+ false,
352
+ false,
353
+ ))
354
+ })
355
+ })
356
+
357
+ describe("the cross-table single door", () => {
358
+ let response = (~multi) =>
359
+ F.resolvedFieldResponse(
360
+ ~multi,
361
+ ~ownerField=Some("customerId"),
362
+ ~elevatedGroups=elevated,
363
+ ~retiredField="shelfStatus",
364
+ ~retiredValues=["Archived"],
365
+ )
366
+
367
+ testSync("narrows the one row it hands back", () =>
368
+ expect(response(~multi=false)->String.includes("_owns(_row) && _live(_row) ? _row : null"))
369
+ ->Expect.toBe(true)
370
+ )
371
+
372
+ testSync("narrows every row of the list form", () =>
373
+ expect(
374
+ response(~multi=true)->String.includes("filter(_row => _owns(_row) && _live(_row))"),
375
+ )->Expect.toBe(true)
376
+ )
377
+
378
+ // The list form used to answer with the first row alone, whatever the SDL said.
379
+ testSync("the list form returns the list", () =>
380
+ expect(response(~multi=true)->String.includes("ctx.result.items[0]"))->Expect.toBe(false)
381
+ )
382
+
383
+ testSync("a target with neither rule keeps the plain response", () => {
384
+ let plain = F.resolvedFieldResponse(~multi=false, ~ownerField=None, ~elevatedGroups=elevated)
385
+ expect((plain->String.includes("_owns"), plain->String.includes("ctx.result.items[0]")))
386
+ ->toEqual((false, true))
387
+ })
388
+ })
@@ -206,6 +206,58 @@ globalThis.describe("the unowned stub is callable where the door calls it", () =
206
206
  });
207
207
  });
208
208
 
209
+ globalThis.describe("the cross-table batch door", () => {
210
+ let code = AppSync_Resolver_Functions$PulumiAws.resolveIds("productIds", undefined, undefined, "shelfStatus", ["Archived"], elevated)("ProductsTable");
211
+ globalThis.test("returns the rows out of the batch envelope", () => {
212
+ globalThis.expect(code.includes("ctx.result?.data?.['ProductsTable']")).toBe(true);
213
+ });
214
+ globalThis.test("drops retired rows from the returned array", () => {
215
+ globalThis.expect(code.includes("item !== null && _live(item)")).toBe(true);
216
+ });
217
+ globalThis.test("short-circuits an empty id array instead of reading", () => {
218
+ globalThis.expect([
219
+ code.includes("if (idList.length === 0) return runtime.earlyReturn([]);"),
220
+ code.includes("operation: 'GetItem'")
221
+ ]).toEqual([
222
+ true,
223
+ false
224
+ ]);
225
+ });
226
+ globalThis.test("is unchanged for a target that declares neither rule", () => {
227
+ let plain = AppSync_Resolver_Functions$PulumiAws.resolveIds("productIds", undefined, undefined, undefined, undefined, undefined)("ProductsTable");
228
+ globalThis.expect([
229
+ plain.includes("_owns"),
230
+ plain.includes("_live")
231
+ ]).toEqual([
232
+ false,
233
+ false
234
+ ]);
235
+ });
236
+ });
237
+
238
+ globalThis.describe("the cross-table single door", () => {
239
+ let response = multi => AppSync_Resolver_Functions$PulumiAws.resolvedFieldResponse(multi, "customerId", elevated, "shelfStatus", ["Archived"]);
240
+ globalThis.test("narrows the one row it hands back", () => {
241
+ globalThis.expect(response(false).includes("_owns(_row) && _live(_row) ? _row : null")).toBe(true);
242
+ });
243
+ globalThis.test("narrows every row of the list form", () => {
244
+ globalThis.expect(response(true).includes("filter(_row => _owns(_row) && _live(_row))")).toBe(true);
245
+ });
246
+ globalThis.test("the list form returns the list", () => {
247
+ globalThis.expect(response(true).includes("ctx.result.items[0]")).toBe(false);
248
+ });
249
+ globalThis.test("a target with neither rule keeps the plain response", () => {
250
+ let plain = AppSync_Resolver_Functions$PulumiAws.resolvedFieldResponse(false, undefined, elevated, undefined, undefined);
251
+ globalThis.expect([
252
+ plain.includes("_owns"),
253
+ plain.includes("ctx.result.items[0]")
254
+ ]).toEqual([
255
+ false,
256
+ true
257
+ ]);
258
+ });
259
+ });
260
+
209
261
  let F;
210
262
 
211
263
  export {
@@ -612,3 +612,169 @@ describe("owner scoping", () => {
612
612
  expect(r->ids)->toEqual(["p-1", "p-3"])
613
613
  })
614
614
  })
615
+
616
+ // ── nested union member types ───────────────────────────────────────────────
617
+ //
618
+ // The `__typename` a union member is resolved from is written into the stored
619
+ // row, above the DynamoDB/Postgres branch (`withUnionMemberTypes` wraps the
620
+ // result of `makeQueryDbOps`), and this resolver stamps only the *top level* of
621
+ // a `node` answer. So the Postgres door's whole claim is that it hands a nested
622
+ // union back exactly as stored — which no test made, and which no deploy can
623
+ // make either: nothing runs the Postgres backend, so this door cannot be called
624
+ // against a deployment the way the AppSync ones were.
625
+ //
626
+ // Losing the nested key is not visible as an error: the member resolves to null
627
+ // and the null takes its non-nullable parent, so rows leave the answer.
628
+
629
+ let geoRow = (id, status) => {
630
+ let geo = Dict.make()
631
+ geo->Dict.set("__typename", JSON.Encode.string("GeolocationLocated"))
632
+ geo->Dict.set("lat", JSON.Encode.float(48.2082))
633
+ geo->Dict.set("lng", JSON.Encode.float(16.3738))
634
+ let o = Dict.make()
635
+ o->Dict.set("id", JSON.Encode.string(id))
636
+ o->Dict.set("status", JSON.Encode.string(status))
637
+ o->Dict.set("name", JSON.Encode.string("Geo " ++ id))
638
+ o->Dict.set("owner", JSON.Encode.string("u-a"))
639
+ o->Dict.set("geolocation", JSON.Encode.object(geo))
640
+ JSON.Encode.object(o)
641
+ }
642
+
643
+ let geoStore = Dict.fromArray([("g-1", geoRow("g-1", "active")), ("g-2", geoRow("g-2", "active"))])
644
+ let geoItems = () => geoStore->Dict.valuesToArray
645
+
646
+ let geoConnection = (): JSON.t =>
647
+ JSON.Encode.object(
648
+ Dict.fromArray([
649
+ (
650
+ "edges",
651
+ JSON.Encode.array(
652
+ geoItems()->Array.map(item =>
653
+ JSON.Encode.object(
654
+ Dict.fromArray([("cursor", JSON.Encode.string("c")), ("node", item)]),
655
+ )
656
+ ),
657
+ ),
658
+ ),
659
+ ]),
660
+ )
661
+
662
+ let geoBinding = (): PgQueryResolver_Lambda.binding => {
663
+ ops: {
664
+ ...ops,
665
+ load: async id => Ok(geoStore->Dict.get(id)->Option.mapOr([], i => [i])),
666
+ },
667
+ pushdowns: {
668
+ indexLookup: async (~readModelName as _, _field, _value) => geoItems(),
669
+ byIds: async (~readModelName as _, ids) => ids->Array.filterMap(id => geoStore->Dict.get(id)),
670
+ listPage: async (
671
+ ~readModelName as _,
672
+ ~argsDict as _,
673
+ ~capability as _,
674
+ ~labelField as _,
675
+ ~ownerScope as _=?,
676
+ ~retiredScope as _=?,
677
+ ) => Some(geoConnection()),
678
+ itemsPage: async (
679
+ ~readModelName as _,
680
+ ~subIdField as _,
681
+ ~id as _,
682
+ ~argsDict as _,
683
+ ~ownerScope as _=?,
684
+ ~retiredScope as _=?,
685
+ ) => geoConnection(),
686
+ scanAll: async (~readModelName as _) => geoItems(),
687
+ },
688
+ indexes: [
689
+ {index: "byStatus", type_: "S", idField: "status", projectionType: Reventless.ReadModel.ALL},
690
+ ],
691
+ subIdField: Some("lineNo"),
692
+ capability,
693
+ labelField: "name",
694
+ includeIdParam: true,
695
+ authorization: Reventless.Authorization.AllowAnonymous,
696
+ ownerField: None,
697
+ retiredField: None,
698
+ retiredValues: None,
699
+ namedWhenRetired: false,
700
+ }
701
+
702
+ // The member type of the first row a door answers with, whatever shape it took.
703
+ let memberOf = (r: JSON.t): option<string> => {
704
+ let fromRow = row =>
705
+ row->field("geolocation")->Option.flatMap(g => g->str("__typename"))
706
+ switch r->JSON.Decode.array {
707
+ | Some(rows) => rows->Array.get(0)->Option.flatMap(fromRow)
708
+ | None =>
709
+ switch r->field("edges")->Option.flatMap(JSON.Decode.array) {
710
+ | Some(edges) => edges->Array.get(0)->Option.flatMap(e => e->field("node"))->Option.flatMap(fromRow)
711
+ | None => fromRow(r)
712
+ }
713
+ }
714
+ }
715
+
716
+ describe("PgQueryResolver — a nested union survives every door", () => {
717
+ let doors = [
718
+ ("getById", mkPayload(~kind="getById", ~args=objArgs([("id", JSON.Encode.string("g-1"))]), ())),
719
+ (
720
+ "byIds",
721
+ mkPayload(~kind="byIds", ~args=objArgs([("ids", JSON.Encode.array([JSON.Encode.string("g-1")]))]), ()),
722
+ ),
723
+ (
724
+ "index",
725
+ mkPayload(~kind="index", ~index="byStatus", ~args=objArgs([("byStatus", JSON.Encode.string("active"))]), ()),
726
+ ),
727
+ ("list", mkPayload(~kind="list", ())),
728
+ ("items", mkPayload(~kind="items", ~args=objArgs([("id", JSON.Encode.string("g-1"))]), ())),
729
+ // The cross-table doors answer with the *target's* rows, so a union on the
730
+ // target travels through a field on some other view's type.
731
+ (
732
+ "resolveOne",
733
+ {
734
+ readModelName: "Geos",
735
+ kind: "resolveOne",
736
+ target: "Geos",
737
+ source: objArgs([("ref", JSON.Encode.string("g-1"))]),
738
+ sourceIdField: "ref",
739
+ multi: false,
740
+ arguments: objArgs([]),
741
+ identity: Reventless.Identity.anonymous,
742
+ },
743
+ ),
744
+ (
745
+ "resolveMany",
746
+ {
747
+ readModelName: "Geos",
748
+ kind: "resolveMany",
749
+ target: "Geos",
750
+ source: objArgs([("refs", JSON.Encode.array([JSON.Encode.string("g-1")]))]),
751
+ sourceIdsField: "refs",
752
+ arguments: objArgs([]),
753
+ identity: Reventless.Identity.anonymous,
754
+ },
755
+ ),
756
+ ]
757
+
758
+ doors->Array.forEach(((label, payload)) =>
759
+ testPromise(label ++ " hands the member type back as stored", async () => {
760
+ let r = await PgQueryResolver_Lambda.dispatch(~binding=geoBinding(), ~payload)
761
+ expect(memberOf(r))->toEqual(Some("GeolocationLocated"))
762
+ })
763
+ )
764
+
765
+ // `node` is the one door that writes a `__typename` of its own, at the top
766
+ // level. It must not reach down and overwrite the member's.
767
+ testPromise("node stamps the top level without disturbing the member", async () => {
768
+ PgQueryResolver_Lambda.register(~readModelName="Geos", geoBinding())
769
+ PgQueryResolver_Lambda.registerNodeType(~typeName="Geo", ~readModelName="Geos")
770
+ let payload: PgQueryResolver_Lambda.payload = {
771
+ readModelName: "",
772
+ kind: "node",
773
+ arguments: objArgs([("id", JSON.Encode.string(btoa("Geo:g-1")))]),
774
+ identity: Reventless.Identity.anonymous,
775
+ }
776
+ let r = await PgQueryResolver_Lambda.handler(payload, Obj.magic(Nullable.null))
777
+ expect(r->str("__typename"))->toEqual(Some("Geo"))
778
+ expect(memberOf(r))->toEqual(Some("GeolocationLocated"))
779
+ })
780
+ })
@@ -629,6 +629,196 @@ globalThis.describe("owner scoping", () => {
629
629
  });
630
630
  });
631
631
 
632
+ function geoRow(id, status) {
633
+ let geo = {};
634
+ geo["__typename"] = "GeolocationLocated";
635
+ geo["lat"] = 48.2082;
636
+ geo["lng"] = 16.3738;
637
+ let o = {};
638
+ o["id"] = id;
639
+ o["status"] = status;
640
+ o["name"] = "Geo " + id;
641
+ o["owner"] = "u-a";
642
+ o["geolocation"] = geo;
643
+ return o;
644
+ }
645
+
646
+ let geoStore = Object.fromEntries([
647
+ [
648
+ "g-1",
649
+ geoRow("g-1", "active")
650
+ ],
651
+ [
652
+ "g-2",
653
+ geoRow("g-2", "active")
654
+ ]
655
+ ]);
656
+
657
+ function geoItems() {
658
+ return Object.values(geoStore);
659
+ }
660
+
661
+ function geoConnection() {
662
+ return Object.fromEntries([[
663
+ "edges",
664
+ Object.values(geoStore).map(item => Object.fromEntries([
665
+ [
666
+ "cursor",
667
+ "c"
668
+ ],
669
+ [
670
+ "node",
671
+ item
672
+ ]
673
+ ]))
674
+ ]]);
675
+ }
676
+
677
+ function geoBinding() {
678
+ return {
679
+ ops: {
680
+ load: async id => ({
681
+ TAG: "Ok",
682
+ _0: Stdlib_Option.mapOr(geoStore[id], [], i => [i])
683
+ }),
684
+ loadStream: 0,
685
+ save: 0,
686
+ saveBatch: 0,
687
+ count: 0,
688
+ delete: 0,
689
+ deleteBatch: 0
690
+ },
691
+ pushdowns: {
692
+ indexLookup: async (param, _field, _value) => Object.values(geoStore),
693
+ byIds: async (param, ids) => Stdlib_Array.filterMap(ids, id => geoStore[id]),
694
+ listPage: async (param, param$1, param$2, param$3, param$4, param$5) => geoConnection(),
695
+ itemsPage: async (param, param$1, param$2, param$3, param$4, param$5) => geoConnection(),
696
+ scanAll: async param => Object.values(geoStore)
697
+ },
698
+ indexes: [{
699
+ index: "byStatus",
700
+ type_: "S",
701
+ idField: "status",
702
+ projectionType: "ALL"
703
+ }],
704
+ subIdField: "lineNo",
705
+ capability: capability,
706
+ labelField: "name",
707
+ includeIdParam: true,
708
+ authorization: "AllowAnonymous",
709
+ ownerField: undefined,
710
+ retiredField: undefined,
711
+ retiredValues: undefined,
712
+ namedWhenRetired: false
713
+ };
714
+ }
715
+
716
+ function memberOf(r) {
717
+ let fromRow = row => Stdlib_Option.flatMap(field(row, "geolocation"), g => str(g, "__typename"));
718
+ let rows = Stdlib_JSON.Decode.array(r);
719
+ if (rows !== undefined) {
720
+ return Stdlib_Option.flatMap(rows[0], fromRow);
721
+ }
722
+ let edges = Stdlib_Option.flatMap(field(r, "edges"), Stdlib_JSON.Decode.array);
723
+ if (edges !== undefined) {
724
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(edges[0], e => field(e, "node")), fromRow);
725
+ } else {
726
+ return fromRow(r);
727
+ }
728
+ }
729
+
730
+ globalThis.describe("PgQueryResolver — a nested union survives every door", () => {
731
+ let doors = [
732
+ [
733
+ "getById",
734
+ mkPayload("getById", undefined, Object.fromEntries([[
735
+ "id",
736
+ "g-1"
737
+ ]]), undefined, undefined)
738
+ ],
739
+ [
740
+ "byIds",
741
+ mkPayload("byIds", undefined, Object.fromEntries([[
742
+ "ids",
743
+ ["g-1"]
744
+ ]]), undefined, undefined)
745
+ ],
746
+ [
747
+ "index",
748
+ mkPayload("index", "byStatus", Object.fromEntries([[
749
+ "byStatus",
750
+ "active"
751
+ ]]), undefined, undefined)
752
+ ],
753
+ [
754
+ "list",
755
+ mkPayload("list", undefined, undefined, undefined, undefined)
756
+ ],
757
+ [
758
+ "items",
759
+ mkPayload("items", undefined, Object.fromEntries([[
760
+ "id",
761
+ "g-1"
762
+ ]]), undefined, undefined)
763
+ ],
764
+ [
765
+ "resolveOne",
766
+ {
767
+ readModelName: "Geos",
768
+ kind: "resolveOne",
769
+ target: "Geos",
770
+ source: Object.fromEntries([[
771
+ "ref",
772
+ "g-1"
773
+ ]]),
774
+ sourceIdField: "ref",
775
+ multi: false,
776
+ arguments: Object.fromEntries([]),
777
+ identity: Identity$Reventless.anonymous
778
+ }
779
+ ],
780
+ [
781
+ "resolveMany",
782
+ {
783
+ readModelName: "Geos",
784
+ kind: "resolveMany",
785
+ target: "Geos",
786
+ source: Object.fromEntries([[
787
+ "refs",
788
+ ["g-1"]
789
+ ]]),
790
+ sourceIdsField: "refs",
791
+ arguments: Object.fromEntries([]),
792
+ identity: Identity$Reventless.anonymous
793
+ }
794
+ ]
795
+ ];
796
+ doors.forEach(param => {
797
+ let payload = param[1];
798
+ globalThis.test(param[0] + " hands the member type back as stored", async () => {
799
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(geoBinding(), undefined, payload);
800
+ globalThis.expect(memberOf(r)).toEqual("GeolocationLocated");
801
+ });
802
+ });
803
+ globalThis.test("node stamps the top level without disturbing the member", async () => {
804
+ PgQueryResolver_Lambda$ReventlessAws.register("Geos", geoBinding());
805
+ PgQueryResolver_Lambda$ReventlessAws.registerNodeType("Geo", "Geos");
806
+ let payload_arguments = Object.fromEntries([[
807
+ "id",
808
+ btoa("Geo:g-1")
809
+ ]]);
810
+ let payload = {
811
+ readModelName: "",
812
+ kind: "node",
813
+ arguments: payload_arguments,
814
+ identity: Identity$Reventless.anonymous
815
+ };
816
+ let r = await PgQueryResolver_Lambda$ReventlessAws.handler(payload, null);
817
+ globalThis.expect(str(r, "__typename")).toEqual("Geo");
818
+ globalThis.expect(memberOf(r)).toEqual("GeolocationLocated");
819
+ });
820
+ });
821
+
632
822
  export {
633
823
  mk,
634
824
  store,
@@ -647,5 +837,11 @@ export {
647
837
  field,
648
838
  str,
649
839
  ids,
840
+ geoRow,
841
+ geoStore,
842
+ geoItems,
843
+ geoConnection,
844
+ geoBinding,
845
+ memberOf,
650
846
  }
651
847
  /* store Not a pure module */