@reventlessdev/reventless-aws 3.0.0-alpha.306 → 3.0.0-alpha.308

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.308 (2026-08-18)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** make the unowned stub callable on the doors that call it ([931aa39](https://github.com/ReventlessDev/reventless-core/commit/931aa3968ae0e29befab7183125ed2453d4765cb))
11
+
12
+
13
+ # 3.0.0-alpha.307 (2026-08-18)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **api:** make the by-index door answer, and let an elevated caller widen it ([0fe0c6f](https://github.com/ReventlessDev/reventless-core/commit/0fe0c6f8dec6228ecaba39577e28d780b4f79c83))
18
+ * **aws:** apply [@owner](https://github.com/owner) on the DynamoDB by-ids and by-index doors ([a6b5afc](https://github.com/ReventlessDev/reventless-core/commit/a6b5afc2923bfa4d3e0b2990367f67e8ffdd8877))
19
+ * **aws:** narrow retirement on every DynamoDB door, not only the list ([d6a799b](https://github.com/ReventlessDev/reventless-core/commit/d6a799b287b28e9c1f75e193adbf3f6328a6bf2d))
20
+ ### Features
21
+
22
+ * **core:** let a reference name a retired row, and let an elevated caller open one ([9e2623a](https://github.com/ReventlessDev/reventless-core/commit/9e2623a4b22487561607fcc0ca19d51726069ee4))
23
+
24
+
6
25
  # 3.0.0-alpha.306 (2026-08-16)
7
26
 
8
27
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.306",
3
+ "version": "3.0.0-alpha.308",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -12,18 +12,18 @@
12
12
  "@aws-sdk/s3-request-presigner": "3.970.0",
13
13
  "sury": "11.0.0-alpha.4",
14
14
  "uuid": "^13.0.0",
15
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
15
+ "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.12",
16
+ "@reventlessdev/rescript-node": "2.0.0-alpha.8",
16
17
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
17
- "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.11",
18
- "@reventlessdev/rescript-node": "2.0.0-alpha.7",
19
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.78",
20
- "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
18
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.80",
19
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
21
20
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
22
- "@reventlessdev/reventless-core": "3.0.0-alpha.238",
23
- "@reventlessdev/reventless-infra": "3.0.0-alpha.144",
24
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.102",
25
- "@reventlessdev/reventless-spec": "3.0.0-alpha.116",
26
- "@reventlessdev/reventless-interop": "3.0.0-alpha.31"
21
+ "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
+ "@reventlessdev/reventless-core": "3.0.0-alpha.239",
23
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.145",
24
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.31",
25
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.117",
26
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.103"
27
27
  },
28
28
  "devDependencies": {
29
29
  "rescript": "12.3.0",
@@ -99,6 +99,10 @@ type binding = {
99
99
  ownerField: option<string>,
100
100
  retiredField: option<string>,
101
101
  retiredValues: option<array<string>>,
102
+ /** Whether a reference to a retired row of this read model still resolves its
103
+ name — the `@namedWhenRetired` opt-in. Read only by the reference door;
104
+ every other door narrows as it always did. */
105
+ namedWhenRetired: bool,
102
106
  }
103
107
 
104
108
  // -- arg helpers -------------------------------------------------------------
@@ -253,6 +257,66 @@ let dispatch = async (
253
257
  Array.concat(found, extra)->Array.filter(item => ownerAllows(item) && retiredAllows(item)),
254
258
  )
255
259
 
260
+ | "refs" =>
261
+ // The reference door. The same by-ids read, projected on the way out to
262
+ // what naming a row needs, with the retirement lifted for a view that said
263
+ // its retired rows keep their names — and the owner rule left exactly where
264
+ // it was, so a row that is somebody's is still only theirs.
265
+ let ids = payload.arguments->argStrs("ids")
266
+ let found = await binding.pushdowns.byIds(~readModelName=rm, ids)
267
+ let missing =
268
+ found->Array.length < ids->Array.length
269
+ ? ids->Array.filterMap(ReventlessCore.Api_Ids.alternateKey)
270
+ : []
271
+ let extra = missing->Array.length > 0
272
+ ? await binding.pushdowns.byIds(~readModelName=rm, missing)
273
+ : []
274
+ let retirementOf = (item: JSON.t) =>
275
+ switch binding.retiredField {
276
+ | None => (false, None)
277
+ | Some(field) =>
278
+ let cell = item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(field))
279
+ let scope: Reventless.OwnerScope.retiredScope = {
280
+ field,
281
+ values: binding.retiredValues,
282
+ }
283
+ let retired = scope->Reventless.OwnerScope.isRetiredValue(cell)
284
+ (
285
+ retired,
286
+ retired && binding.retiredValues->Option.isSome
287
+ ? cell->Option.flatMap(JSON.Decode.string)
288
+ : None,
289
+ )
290
+ }
291
+ JSON.Encode.array(
292
+ Array.concat(found, extra)
293
+ ->Array.filter(ownerAllows)
294
+ ->Array.filterMap(item => {
295
+ let (retired, state) = retirementOf(item)
296
+ if retired && !binding.namedWhenRetired {
297
+ None
298
+ } else {
299
+ let get = key => item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))
300
+ let id = get("id")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
301
+ Some(
302
+ Dict.fromArray([
303
+ ("id", JSON.Encode.string(id)),
304
+ (
305
+ "label",
306
+ JSON.Encode.string(
307
+ get(binding.labelField)
308
+ ->Option.flatMap(JSON.Decode.string)
309
+ ->Option.getOr(id),
310
+ ),
311
+ ),
312
+ ("retired", JSON.Encode.bool(retired)),
313
+ ("retiredState", state->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
314
+ ])->JSON.Encode.object,
315
+ )
316
+ }
317
+ }),
318
+ )
319
+
256
320
  | "items" =>
257
321
  // Sub-id connection: {single}Items(id, filter, first/after/last/before).
258
322
  // Requires a subIdField; without one it's an empty connection.
@@ -185,11 +185,63 @@ async function dispatch(binding, lookupBindingOpt, payload) {
185
185
  let items$1 = await binding.pushdowns.scanAll(rm);
186
186
  return QueryDbListQuery$ReventlessCore.run(items$1, argsDict, binding.capability, binding.labelField, undefined, ownerScope, retiredScope);
187
187
  break;
188
+ case "refs" :
189
+ let ids$1 = argStrs(payload.arguments, "ids");
190
+ let found$2 = await binding.pushdowns.byIds(rm, ids$1);
191
+ let missing$1 = found$2.length < ids$1.length ? Stdlib_Array.filterMap(ids$1, Api_Ids$ReventlessCore.alternateKey) : [];
192
+ let extra$1 = missing$1.length !== 0 ? await binding.pushdowns.byIds(rm, missing$1) : [];
193
+ let retirementOf = item => {
194
+ let field = binding.retiredField;
195
+ if (field === undefined) {
196
+ return [
197
+ false,
198
+ undefined
199
+ ];
200
+ }
201
+ let cell = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[field]);
202
+ let scope_values = binding.retiredValues;
203
+ let scope = {
204
+ field: field,
205
+ values: scope_values
206
+ };
207
+ let retired = OwnerScope$Reventless.isRetiredValue(scope, cell);
208
+ return [
209
+ retired,
210
+ retired && Stdlib_Option.isSome(binding.retiredValues) ? Stdlib_Option.flatMap(cell, Stdlib_JSON.Decode.string) : undefined
211
+ ];
212
+ };
213
+ return Stdlib_Array.filterMap(found$2.concat(extra$1).filter(ownerAllows), item => {
214
+ let match = retirementOf(item);
215
+ let retired = match[0];
216
+ if (retired && !binding.namedWhenRetired) {
217
+ return;
218
+ }
219
+ let get = key => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[key]);
220
+ let id = Stdlib_Option.getOr(Stdlib_Option.flatMap(get("id"), Stdlib_JSON.Decode.string), "");
221
+ return Object.fromEntries([
222
+ [
223
+ "id",
224
+ id
225
+ ],
226
+ [
227
+ "label",
228
+ Stdlib_Option.getOr(Stdlib_Option.flatMap(get(binding.labelField), Stdlib_JSON.Decode.string), id)
229
+ ],
230
+ [
231
+ "retired",
232
+ retired
233
+ ],
234
+ [
235
+ "retiredState",
236
+ Stdlib_Option.mapOr(match[1], null, prim => prim)
237
+ ]
238
+ ]);
239
+ });
188
240
  case "resolveMany" :
189
241
  let target = Stdlib_Option.getOr(payload.target, rm);
190
242
  let source = Stdlib_Option.getOr(payload.source, null);
191
- let ids$1 = argStrs(source, Stdlib_Option.getOr(payload.sourceIdsField, ""));
192
- return await binding.pushdowns.byIds(target, ids$1);
243
+ let ids$2 = argStrs(source, Stdlib_Option.getOr(payload.sourceIdsField, ""));
244
+ return await binding.pushdowns.byIds(target, ids$2);
193
245
  case "resolveOne" :
194
246
  let target$1 = Stdlib_Option.getOr(payload.target, rm);
195
247
  let source$1 = Stdlib_Option.getOr(payload.source, null);
@@ -184,13 +184,38 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
184
184
  }
185
185
  let elevatedGroups = Reventless.OwnerScope.elevatedGroups()
186
186
 
187
+ // Read here for exactly the reason `ownerField` above is, and after making
188
+ // the same mistake: the by-id, by-ids and by-index resolvers are built below
189
+ // and this lookup used to sit past all of them, so they were generated
190
+ // without a retirement predicate — a list that withheld archived rows beside
191
+ // by-key doors that handed any one of them over on request.
192
+ let retiredSpec =
193
+ stateSchemaOpt
194
+ ->Option.flatMap(Reventless.StateAnnotations.getSpec)
195
+ ->Option.flatMap(spec => spec.retired)
196
+ let retiredField = retiredSpec->Option.map(r => r.field)
197
+ let retiredValues = retiredSpec->Option.flatMap(r => r.values)
198
+
187
199
  let resolverByIdSingle = if includeIdParam {
188
200
  makeQueryResolver(
189
201
  ~resolverName=fieldNameForSingle->String.capitalize,
190
202
  ~field=fieldNameForSingle->Pulumi.Input.make,
191
203
  ~code=switch subIdField {
192
- | Some(sortField) => Resolver.Functions.queryByIdSort(sortField, ~ownerField?, ~elevatedGroups)
193
- | None => Resolver.Functions.getItemById(~ownerField?, ~elevatedGroups)
204
+ | Some(sortField) =>
205
+ Resolver.Functions.queryByIdSort(
206
+ sortField,
207
+ ~ownerField?,
208
+ ~elevatedGroups,
209
+ ~retiredField?,
210
+ ~retiredValues?,
211
+ )
212
+ | None =>
213
+ Resolver.Functions.getItemById(
214
+ ~ownerField?,
215
+ ~elevatedGroups,
216
+ ~retiredField?,
217
+ ~retiredValues?,
218
+ )
194
219
  },
195
220
  )
196
221
  } else {
@@ -285,12 +310,6 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
285
310
  }
286
311
  | None => ()
287
312
  }
288
- let retiredSpec =
289
- stateSchemaOpt
290
- ->Option.flatMap(Reventless.StateAnnotations.getSpec)
291
- ->Option.flatMap(spec => spec.retired)
292
- let retiredField = retiredSpec->Option.map(r => r.field)
293
- let retiredValues = retiredSpec->Option.flatMap(r => r.values)
294
313
  // The same class of "works, but scans" mistake as the owner warning above,
295
314
  // and the retirement case degrades the same way: the FilterExpression is
296
315
  // applied after the page is read, so pages shrink as the archive's share of
@@ -333,26 +352,16 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
333
352
  },
334
353
  )
335
354
  let resolversByIndex = indexes->Array.map(({index} as indexConfig) => {
336
- // Strip a leading "by" from the index name before capitalizing to avoid
337
- // double "By" in the generated field name (e.g. index "byPlugin" "ByPlugin",
338
- // not "ByByPlugin"). Uses the plugin-prefixed fieldNameForSingle so the
339
- // resolver name is properly namespaced (e.g. "schemaHistoryByPlugin", not
340
- // "SchemaHistoryByByPlugin" from the raw entity name).
341
- let stripLeadingBy = s =>
342
- if s->String.startsWith("by") && s->String.length > 2 {
343
- s->String.slice(~start=2, ~end=s->String.length)
344
- } else {
345
- s
346
- }
347
- let resolverName =
348
- fieldNameForSingle->String.capitalize ++
349
- "By" ++
350
- (index->stripLeadingBy->String.capitalize)
351
- let fieldName =
352
- fieldNameForSingle ++
353
- "By" ++
354
- (index->stripLeadingBy->String.capitalize)
355
- let idField = indexConfig.idField->Option.getOr(index)
355
+ // Name and key field both come from `GraphQL_FragmentGenerator`, which is
356
+ // where the SDL field this attaches to is derived. Deriving them here as
357
+ // well is how the two came to disagree: the emitted field declared `id`
358
+ // while this resolver read the index key, so the door could not be called.
359
+ let fieldName = ReventlessCore.GraphQL_FragmentGenerator.indexQueryFieldName(
360
+ ~singleFieldName=fieldNameForSingle,
361
+ ~index,
362
+ )
363
+ let resolverName = fieldName->String.capitalize
364
+ let idField = ReventlessCore.GraphQL_FragmentGenerator.indexKeyField(indexConfig)
356
365
  switch indexConfig.authorization {
357
366
  | None =>
358
367
  makeQueryResolver(
@@ -360,8 +369,24 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
360
369
  ~field=fieldName->Pulumi.Input.make,
361
370
  ~code=switch indexConfig.subIdField {
362
371
  | Some(sortField) =>
363
- Resolver.Functions.queryByIndexSortFiltered(~index, ~idField, ~sortField)
364
- | None => Resolver.Functions.queryByIndexFiltered(~index, ~idField)
372
+ Resolver.Functions.queryByIndexSortFiltered(
373
+ ~index,
374
+ ~idField,
375
+ ~sortField,
376
+ ~ownerField?,
377
+ ~retiredField?,
378
+ ~retiredValues?,
379
+ ~elevatedGroups,
380
+ )
381
+ | None =>
382
+ Resolver.Functions.queryByIndexFiltered(
383
+ ~index,
384
+ ~idField,
385
+ ~ownerField?,
386
+ ~retiredField?,
387
+ ~retiredValues?,
388
+ ~elevatedGroups,
389
+ )
365
390
  },
366
391
  )
367
392
  | Some({tableName, group}) =>
@@ -387,7 +412,20 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
387
412
  ~name=resolverName,
388
413
  ~api,
389
414
  ~dataSource=dataSourceName,
390
- ~code=Resolver.Functions.queryByIndexFiltered(~index, ~idField),
415
+ // No `~ownerField`, deliberately: this index declares its own
416
+ // authorization, and the rows it grants a group member are by
417
+ // construction owned by somebody else — an order assigned to a
418
+ // fulfilment operator belongs to the customer who placed it. Adding the
419
+ // owner predicate here would return nothing and revoke the access the
420
+ // auth table exists to grant. Retirement still applies: an archived row
421
+ // is withdrawn from everyone who has not asked, whoever owns it.
422
+ ~code=Resolver.Functions.queryByIndexFiltered(
423
+ ~index,
424
+ ~idField,
425
+ ~retiredField?,
426
+ ~retiredValues?,
427
+ ~elevatedGroups,
428
+ ),
391
429
  ~opts,
392
430
  )
393
431
  // The interceptor leads the chain, as it does in every other Query
@@ -440,7 +478,43 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
440
478
  ~field=byIdsField->Pulumi.Input.make,
441
479
  ~code=generateCode(
442
480
  ~storageResource=storage,
443
- ~template=Resolver.Functions.batchGetItemsByIds,
481
+ ~template=Resolver.Functions.batchGetItemsByIds(
482
+ ~ownerField?,
483
+ ~retiredField?,
484
+ ~retiredValues?,
485
+ ~elevatedGroups,
486
+ ),
487
+ ),
488
+ ),
489
+ )
490
+ } else {
491
+ None
492
+ }
493
+
494
+ // The reference door. Registered under the same conditions as by-ids because
495
+ // it is the same read; what differs is the projection on the way out, and
496
+ // that a view which named its retired rows lets them through it.
497
+ //
498
+ // Not conditional on the annotation: `GraphQL_FragmentGenerator` emits the
499
+ // field for every view — one SDL serves every backend — so a view without a
500
+ // resolver here would be a field that errors rather than one that is absent.
501
+ let resolverRefs = if includeIdParam && subIdField === None {
502
+ let storage = storageResource(~pluginName=None, ~tableName=name)
503
+ let refsField = fieldNameForAll ++ "Refs"
504
+ Some(
505
+ makeQueryResolver(
506
+ ~resolverName=refsField->String.capitalize,
507
+ ~field=refsField->Pulumi.Input.make,
508
+ ~code=generateCode(
509
+ ~storageResource=storage,
510
+ ~template=Resolver.Functions.refsByIds(
511
+ ~labelField,
512
+ ~retiredField,
513
+ ~retiredValues,
514
+ ~namedWhenRetired=retiredSpec->Option.mapOr(false, r => r.namedWhenRetired),
515
+ ~ownerField?,
516
+ ~elevatedGroups,
517
+ ),
444
518
  ),
445
519
  ),
446
520
  )
@@ -535,7 +609,11 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
535
609
  | (None, Some(byIds)) => [resolverByIdSingle, resolverAll, byIds]
536
610
  | (None, None) => [resolverByIdSingle, resolverAll]
537
611
  }
538
- Array.flat([mainResolvers, resolversByIndex, idResolvers, idsResolvers])
612
+ // Appended rather than folded into `mainResolvers`' four-way switch: the door
613
+ // is independent of whether this view has a sub-id door, and adding a third
614
+ // dimension to that match would spell eight cases to say one thing.
615
+ let refsResolvers = resolverRefs->Option.mapOr([], r => [r])
616
+ Array.flat([mainResolvers, refsResolvers, resolversByIndex, idResolvers, idsResolvers])
539
617
  ->Array.map(Util.AppSync.toResourceNative)
540
618
  }
541
619
 
@@ -94,7 +94,10 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
94
94
  let stateSchemaOpt = Plugin_Helpers$ReventlessCore.stateSchemaRegistry[name$1];
95
95
  let ownerField = stateSchemaOpt !== undefined ? Owner$Reventless.fieldNames(stateSchemaOpt)[0] : undefined;
96
96
  let elevatedGroups = OwnerScope$Reventless.elevatedGroups();
97
- let resolverByIdSingle = includeIdParam ? makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, subIdField !== undefined ? AppSync_Resolver_Functions$PulumiAws.queryByIdSort(subIdField, ownerField, elevatedGroups) : AppSync_Resolver_Functions$PulumiAws.getItemById(ownerField, elevatedGroups)) : makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, AppSync_Resolver_Functions$PulumiAws.listAllItems);
97
+ let retiredSpec = Stdlib_Option.flatMap(Stdlib_Option.flatMap(stateSchemaOpt, StateAnnotations$Reventless.getSpec), spec => spec.retired);
98
+ let retiredField = Stdlib_Option.map(retiredSpec, r => r.field);
99
+ let retiredValues = Stdlib_Option.flatMap(retiredSpec, r => r.values);
100
+ let resolverByIdSingle = includeIdParam ? makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, subIdField !== undefined ? AppSync_Resolver_Functions$PulumiAws.queryByIdSort(subIdField, ownerField, elevatedGroups, retiredField, retiredValues) : AppSync_Resolver_Functions$PulumiAws.getItemById(ownerField, elevatedGroups, retiredField, retiredValues)) : makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, AppSync_Resolver_Functions$PulumiAws.listAllItems);
98
101
  let resolverByIdMultiple = includeIdParam ? Stdlib_Option.map(subIdField, sortField => makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle) + "Items", fieldNameForSingle + "Items", AppSync_Resolver_Functions$PulumiAws.queryItemsWithSortConditions(sortField, ownerField, elevatedGroups))) : undefined;
99
102
  let labelField = registryEntry !== undefined ? Stdlib_Option.getOr(registryEntry.labelField, "id") : "id";
100
103
  let capability = stateSchemaOpt !== undefined ? GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(name$1, stateSchemaOpt) : GraphQL_FragmentGenerator$ReventlessCore.emptyCapability;
@@ -123,30 +126,20 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
123
126
  if (ownerField !== undefined && !isIndexed(ownerField)) {
124
127
  log.warn("QueryDbResolvers_AppSync", undefined, name$1 + `: @owner field "` + ownerField + `" is not the key of any index on this table. ` + "Owner-scoped reads will Scan and filter, so pages shrink as the caller's share of the rows falls. Add an @index on that field before this read model grows.");
125
128
  }
126
- let retiredSpec = Stdlib_Option.flatMap(Stdlib_Option.flatMap(stateSchemaOpt, StateAnnotations$Reventless.getSpec), spec => spec.retired);
127
- let retiredField = Stdlib_Option.map(retiredSpec, r => r.field);
128
- let retiredValues = Stdlib_Option.flatMap(retiredSpec, r => r.values);
129
129
  if (retiredField !== undefined && !isIndexed(retiredField)) {
130
130
  log.warn("QueryDbResolvers_AppSync", undefined, name$1 + `: @retired field "` + retiredField + `" is not the key of any index on this table. ` + "Reads that exclude retired rows will Scan and filter, so pages shrink as the archive's share of the rows grows. Add an @index on that field before this read model grows.");
131
131
  }
132
132
  let resolverAll = makeQueryResolver(Stdlib_String.capitalize(fieldNameForAll), fieldNameForAll, connectionSpec ? AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection(labelField, filterFieldNames, rangeFieldNames, sortFieldNames, requireAttribute, ownerField, elevatedGroups, retiredField, retiredValues) : AppSync_Resolver_Functions$PulumiAws.listAllItems);
133
133
  let resolversByIndex = indexes.map(indexConfig => {
134
134
  let index = indexConfig.index;
135
- let stripLeadingBy = s => {
136
- if (s.startsWith("by") && s.length > 2) {
137
- return s.slice(2, s.length);
138
- } else {
139
- return s;
140
- }
141
- };
142
- let resolverName = Stdlib_String.capitalize(fieldNameForSingle) + "By" + Stdlib_String.capitalize(stripLeadingBy(index));
143
- let fieldName = fieldNameForSingle + "By" + Stdlib_String.capitalize(stripLeadingBy(index));
144
- let idField = Stdlib_Option.getOr(indexConfig.idField, index);
135
+ let fieldName = GraphQL_FragmentGenerator$ReventlessCore.indexQueryFieldName(fieldNameForSingle, index);
136
+ let resolverName = Stdlib_String.capitalize(fieldName);
137
+ let idField = GraphQL_FragmentGenerator$ReventlessCore.indexKeyField(indexConfig);
145
138
  let match = indexConfig.authorization;
146
139
  if (match !== undefined) {
147
140
  let authDataSource = AppSync_DataSource$PulumiAws.makeDynamoDBDataSourceWithTableName(resolverName + "Auth", api, Util_DynamoDb$ReventlessAws.findResource(Util_QueryDb$ReventlessCore.getLocalStorageResources(allQueryDbs, match.tableName)).name, apiRole, opts);
148
141
  let authFunction = AppSync_Function$PulumiAws.makeJs(resolverName + "Auth", api, authDataSource.name, AppSync_Resolver_Functions$PulumiAws.authorizeIndexedAccess(index, match.group), opts);
149
- let queryFunction = AppSync_Function$PulumiAws.makeJs(resolverName, api, dataSourceName, AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered(index, idField), opts);
142
+ let queryFunction = AppSync_Function$PulumiAws.makeJs(resolverName, api, dataSourceName, AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered(index, idField, undefined, retiredField, retiredValues, elevatedGroups), opts);
150
143
  let interceptorFn = interceptorFunction(resolverName);
151
144
  return AppSync_Resolver_Retrying$ReventlessAws.makePipelineJsResolver(resolverName, api, "Query", fieldName, AppSync_Resolver_Functions$PulumiAws.pipelinePassThrough, interceptorFn !== undefined ? [
152
145
  interceptorFn,
@@ -158,7 +151,7 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
158
151
  ], opts);
159
152
  }
160
153
  let sortField = indexConfig.subIdField;
161
- return makeQueryResolver(resolverName, fieldName, sortField !== undefined ? AppSync_Resolver_Functions$PulumiAws.queryByIndexSortFiltered(index, idField, sortField) : AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered(index, idField));
154
+ return makeQueryResolver(resolverName, fieldName, sortField !== undefined ? AppSync_Resolver_Functions$PulumiAws.queryByIndexSortFiltered(index, idField, ownerField, sortField, retiredField, retiredValues, elevatedGroups) : AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered(index, idField, ownerField, retiredField, retiredValues, elevatedGroups));
162
155
  });
163
156
  let storageResource = (pluginName, tableName) => Adapter$ReventlessCore.outputToResource(Util_DynamoDb$ReventlessAws.findResourceInOutput(Plugin_Helpers$ReventlessCore.getStorageResources(allQueryDbs, pluginName, tableName)));
164
157
  let generateCode = (storageResource, template) => storageResource.name.apply(realTableName => template(realTableName));
@@ -166,10 +159,18 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
166
159
  if (includeIdParam && subIdField === undefined) {
167
160
  let storage = storageResource(undefined, name$1);
168
161
  let byIdsField = fieldNameForAll + "ByIds";
169
- resolverByIds = makeQueryResolver(Stdlib_String.capitalize(byIdsField), byIdsField, generateCode(storage, AppSync_Resolver_Functions$PulumiAws.batchGetItemsByIds));
162
+ resolverByIds = makeQueryResolver(Stdlib_String.capitalize(byIdsField), byIdsField, generateCode(storage, AppSync_Resolver_Functions$PulumiAws.batchGetItemsByIds(ownerField, retiredField, retiredValues, elevatedGroups)));
170
163
  } else {
171
164
  resolverByIds = undefined;
172
165
  }
166
+ let resolverRefs;
167
+ if (includeIdParam && subIdField === undefined) {
168
+ let storage$1 = storageResource(undefined, name$1);
169
+ let refsField = fieldNameForAll + "Refs";
170
+ resolverRefs = makeQueryResolver(Stdlib_String.capitalize(refsField), refsField, generateCode(storage$1, AppSync_Resolver_Functions$PulumiAws.refsByIds(labelField, retiredField, retiredValues, Stdlib_Option.mapOr(retiredSpec, false, r => r.namedWhenRetired), ownerField, elevatedGroups)));
171
+ } else {
172
+ resolverRefs = undefined;
173
+ }
173
174
  let idResolvers = idResolverConfigs.map(config => {
174
175
  let target = config.target;
175
176
  let targetId = target.idField;
@@ -252,8 +253,10 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
252
253
  resolverAll
253
254
  ]
254
255
  );
256
+ let refsResolvers = Stdlib_Option.mapOr(resolverRefs, [], r => [r]);
255
257
  return [
256
258
  mainResolvers,
259
+ refsResolvers,
257
260
  resolversByIndex,
258
261
  idResolvers,
259
262
  idsResolvers
@@ -9,7 +9,7 @@
9
9
  //
10
10
  // Field names / includeIdParam / connectionSpec come from the same registry the
11
11
  // AppSync path reads, so the SDL emitted by GraphQL_FragmentGenerator stays in
12
- // lockstep. Kinds covered: getById, list (connection), index, byIds. Deferred to
12
+ // lockstep. Kinds covered: getById, list (connection), index, byIds, refs. Deferred to
13
13
  // B3.2c: items (sub-id connection), @resolves/@resolvesMany, node, auth-table.
14
14
 
15
15
  module Resolver = AppSync_Resolver_Retrying
@@ -150,13 +150,6 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
150
150
  ~opts,
151
151
  )
152
152
 
153
- let stripLeadingBy = s =>
154
- if s->String.startsWith("by") && s->String.length > 2 {
155
- s->String.slice(~start=2, ~end=s->String.length)
156
- } else {
157
- s
158
- }
159
-
160
153
  // Resolvers are deferred into resourcesMaker (created inside the schema-pushed
161
154
  // builderOutputs.apply, like the AppSync path) — so they only exist after the
162
155
  // fields are ACTIVE and after PgQueryResolver_Builder.provision has resolved
@@ -202,14 +195,28 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
202
195
  []
203
196
  }
204
197
 
198
+ // The reference door — {list}Refs(ids). Same shape as by-ids and registered
199
+ // under the same conditions, because it is the same read: a set of keys in,
200
+ // rows out. What differs is on the way out, where the row is projected to
201
+ // what a reference needs and a retirement is lifted for a view that named its
202
+ // rows.
203
+ let refs = if includeIdParam && subIdField === None {
204
+ let refsField = fieldNameForAll ++ "Refs"
205
+ [mkResolver(~resolverName=refsField->String.capitalize, ~field=refsField, ~kind="refs")]
206
+ } else {
207
+ []
208
+ }
209
+
205
210
  // Per-index equality queries: {single}By{Index}. A group-restricted index
206
211
  // (indexConfig.authorization) bakes the auth-table pipeline params — the
207
212
  // Lambda verifies group membership + ownership against the auth read model.
208
213
  let byIndex = indexes->Array.map((ic: indexConfig) => {
209
214
  let index = ic.index
210
- let resolverName =
211
- fieldNameForSingle->String.capitalize ++ "By" ++ index->stripLeadingBy->String.capitalize
212
- let field = fieldNameForSingle ++ "By" ++ index->stripLeadingBy->String.capitalize
215
+ let field = ReventlessCore.GraphQL_FragmentGenerator.indexQueryFieldName(
216
+ ~singleFieldName=fieldNameForSingle,
217
+ ~index,
218
+ )
219
+ let resolverName = field->String.capitalize
213
220
  switch ic.authorization {
214
221
  | Some({tableName, group}) =>
215
222
  mkResolver(
@@ -278,6 +285,7 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
278
285
  [byId, all]
279
286
  ->Array.concat(items)
280
287
  ->Array.concat(byIds)
288
+ ->Array.concat(refs)
281
289
  ->Array.concat(byIndex)
282
290
  ->Array.concat(idResolvers)
283
291
  ->Array.concat(idsResolvers)
@@ -7,6 +7,7 @@ import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/s
7
7
  import * as AppSync_Resolver_Functions$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/AppSync/AppSync_Resolver_Functions.res.mjs";
8
8
  import * as PgQueryResolver_Builder$ReventlessAws from "./PgQueryResolver_Builder.res.mjs";
9
9
  import * as AppSync_Resolver_Retrying$ReventlessAws from "../Api/AppSync_Resolver_Retrying.res.mjs";
10
+ import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
10
11
 
11
12
  let identityBlock = `id != null && id.sub != null
12
13
  ? { userId: id.sub, username: id.username, groups: id.claims?.['cognito:groups'] ?? [], claims: id.claims, provider: 'Cognito' }
@@ -77,13 +78,6 @@ function make(name, api, param, dataSourceName, indexes, subIdField, idResolverC
77
78
  PgQueryResolver_Builder$ReventlessAws.registerNodeType(returnTypeName, name$1);
78
79
  }
79
80
  let mkResolver = (resolverName, field, kind, index, authTable, authGroup) => AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(resolverName, api, dataSourceName, "Query", field, invokeTemplate(name$1, kind, index, authTable, authGroup), opts);
80
- let stripLeadingBy = s => {
81
- if (s.startsWith("by") && s.length > 2) {
82
- return s.slice(2, s.length);
83
- } else {
84
- return s;
85
- }
86
- };
87
81
  let resourcesMaker = _allQueryDbs => {
88
82
  let byId = mkResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, includeIdParam ? "getById" : "list", undefined, undefined, undefined);
89
83
  let all = mkResolver(Stdlib_String.capitalize(fieldNameForAll), fieldNameForAll, "list", undefined, undefined, undefined);
@@ -95,10 +89,17 @@ function make(name, api, param, dataSourceName, indexes, subIdField, idResolverC
95
89
  } else {
96
90
  byIds = [];
97
91
  }
92
+ let refs;
93
+ if (includeIdParam && subIdField === undefined) {
94
+ let refsField = fieldNameForAll + "Refs";
95
+ refs = [mkResolver(Stdlib_String.capitalize(refsField), refsField, "refs", undefined, undefined, undefined)];
96
+ } else {
97
+ refs = [];
98
+ }
98
99
  let byIndex = indexes.map(ic => {
99
100
  let index = ic.index;
100
- let resolverName = Stdlib_String.capitalize(fieldNameForSingle) + "By" + Stdlib_String.capitalize(stripLeadingBy(index));
101
- let field = fieldNameForSingle + "By" + Stdlib_String.capitalize(stripLeadingBy(index));
101
+ let field = GraphQL_FragmentGenerator$ReventlessCore.indexQueryFieldName(fieldNameForSingle, index);
102
+ let resolverName = Stdlib_String.capitalize(field);
102
103
  let match = ic.authorization;
103
104
  if (match !== undefined) {
104
105
  return mkResolver(resolverName, field, "index", index, Stdlib_String.capitalize(match.tableName), match.group);
@@ -150,7 +151,7 @@ function make(name, api, param, dataSourceName, indexes, subIdField, idResolverC
150
151
  return [
151
152
  byId,
152
153
  all
153
- ].concat(items).concat(byIds).concat(byIndex).concat(idResolvers).concat(idsResolvers).map(Util_AppSync$ReventlessAws.toResourceNative);
154
+ ].concat(items).concat(byIds).concat(refs).concat(byIndex).concat(idResolvers).concat(idsResolvers).map(Util_AppSync$ReventlessAws.toResourceNative);
154
155
  };
155
156
  return {
156
157
  resources: [],
@@ -163,6 +163,12 @@ let registerBinding = (
163
163
  ->Reventless.StateAnnotations.getSpec
164
164
  ->Option.flatMap(a => a.retired)
165
165
  ->Option.flatMap(r => r.values),
166
+ // Read off the retirement rather than from a second annotation: a record
167
+ // cannot declare the reach of a retirement it does not have.
168
+ namedWhenRetired: spec.stateSchema
169
+ ->Reventless.StateAnnotations.getSpec
170
+ ->Option.flatMap(a => a.retired)
171
+ ->Option.mapOr(false, r => r.namedWhenRetired),
166
172
  },
167
173
  )
168
174
  logDebug("registered resolver binding for " ++ entry.readModelName, {comp: "PgQueryResolver"})
@@ -77,7 +77,8 @@ function registerBinding(pushdowns, pgConnection, entry, spec) {
77
77
  authorization: spec.authorization,
78
78
  ownerField: Owner$Reventless.fieldNames(spec.stateSchema)[0],
79
79
  retiredField: Stdlib_Option.map(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), r => r.field),
80
- retiredValues: Stdlib_Option.flatMap(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), r => r.values)
80
+ retiredValues: Stdlib_Option.flatMap(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), r => r.values),
81
+ namedWhenRetired: Stdlib_Option.mapOr(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), false, r => r.namedWhenRetired)
81
82
  });
82
83
  HandlerFactoryHelpersMjs.log.debug("registered resolver binding for " + entry.readModelName, {
83
84
  comp: "PgQueryResolver"