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

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.318 (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.317 (2026-08-20)
7
15
 
8
16
  ### 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.318",
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-node": "2.0.0-alpha.8",
19
+ "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.2",
20
20
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
21
+ "@reventlessdev/reventless-core": "3.0.0-alpha.246",
22
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.149",
21
23
  "@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"
24
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.33",
25
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.121",
26
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.110"
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,
@@ -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 {