@reventlessdev/reventless-aws 3.0.0-alpha.302 → 3.0.0-alpha.304

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +8 -8
  3. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +21 -0
  4. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +20 -0
  5. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +41 -3
  6. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +27 -6
  7. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +34 -4
  8. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +16 -5
  9. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +12 -0
  10. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +10 -0
  11. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +4 -1
  12. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +16 -2
  13. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +4 -2
  14. package/src/adapter/Runtime/StateTopicPublish.mjs +38 -5
  15. package/src/adapter/StateTopic/StateTopic_AppSync.res +49 -1
  16. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +28 -3
  17. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res +73 -2
  18. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs +39 -4
  19. package/tests/PgQueryResolver_LambdaTest.res +16 -1
  20. package/tests/PgQueryResolver_LambdaTest.res.mjs +46 -32
  21. package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res +65 -0
  22. package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res.mjs +36 -0
  23. package/tests/QueryDbResolvers_AppSyncTest.res.mjs +3 -3
  24. package/tests/StateChangeDescriptorParityTest.res +82 -1
  25. package/tests/StateChangeDescriptorParityTest.res.mjs +97 -1
  26. package/tests/stateChangeDescriptorParity.mjs +72 -3
@@ -92,6 +92,10 @@ type streamEntry = {
92
92
  tableName: Pulumi.Output.t<string>,
93
93
  streamArn: Pulumi.Output.t<string>,
94
94
  topicName: string,
95
+ // The read model's `@retired` field, resolved at deploy time because the relay
96
+ // Lambda has no plugin registry in process to resolve it at request time.
97
+ retiredField: option<string>,
98
+ retiredValues: option<array<string>>,
95
99
  }
96
100
 
97
101
  // One registry per events API, keyed by `eventsApi.name` (the static Pulumi
@@ -134,6 +138,8 @@ let makeForTable = (
134
138
  ~streamArn: Pulumi.Output.t<string>,
135
139
  ~partitionKeyName: Pulumi.Output.t<string>,
136
140
  ~topicName: string,
141
+ ~retiredField: option<string>=?,
142
+ ~retiredValues: option<array<string>>=?,
137
143
  ~eventsApi: AppSync_EventsApi.t,
138
144
  ~opts as _: Pulumi.CustomResourceOptions.t,
139
145
  ) => {
@@ -150,7 +156,12 @@ let makeForTable = (
150
156
 
151
157
  let key = eventsApi.name
152
158
  let entries = registry->Dict.get(key)->Option.getOr([])
153
- registry->Dict.set(key, entries->Array.concat([{tableName: checkedTableName, streamArn, topicName}]))
159
+ registry->Dict.set(
160
+ key,
161
+ entries->Array.concat([
162
+ {tableName: checkedTableName, streamArn, topicName, retiredField, retiredValues},
163
+ ]),
164
+ )
154
165
  }
155
166
 
156
167
  let make = (
@@ -176,6 +187,18 @@ let make = (
176
187
  // `makeForTable` and can be keyed anything.
177
188
  ~partitionKeyName=StateTopic_AppSync_Helpers.entityKeyPartitionAttribute->Pulumi.Output.make,
178
189
  ~topicName,
190
+ // Resolved here, where the read model's name is known and its spec is
191
+ // registered in this process. The relay Lambda has neither.
192
+ ~retiredField=?ReventlessCore.Plugin_Helpers.stateSchemaRegistry
193
+ ->Dict.get(readModelName)
194
+ ->Option.flatMap(Reventless.StateAnnotations.getSpec)
195
+ ->Option.flatMap(spec => spec.retired)
196
+ ->Option.map(r => r.field),
197
+ ~retiredValues=?ReventlessCore.Plugin_Helpers.stateSchemaRegistry
198
+ ->Dict.get(readModelName)
199
+ ->Option.flatMap(Reventless.StateAnnotations.getSpec)
200
+ ->Option.flatMap(spec => spec.retired)
201
+ ->Option.flatMap(r => r.values),
179
202
  ~eventsApi,
180
203
  ~opts,
181
204
  )
@@ -270,6 +293,30 @@ let finish = (
270
293
  dict->JSON.Encode.object->JSON.stringify
271
294
  })
272
295
 
296
+ // STATE_RETIRED_MAP env var — `{ <tableName>: {field, values?} }`, carrying
297
+ // only the tables that declare retirement. Built from the same awaited table
298
+ // names as the topic map so the two cannot describe different sets of tables.
299
+ // The whole predicate travels: the relay Lambda has no plugin registry, so a
300
+ // field without its states would leave it unable to tell the two forms apart.
301
+ let retiredMapJson =
302
+ entries
303
+ ->Array.map(e => e.tableName)
304
+ ->Pulumi.Output.all
305
+ ->Pulumi.Output.apply(tableNames => {
306
+ let dict = Dict.make()
307
+ tableNames->Array.forEachWithIndex((tableName, i) => {
308
+ let entry = entries->Array.getUnsafe(i)
309
+ entry.retiredField->Option.forEach(f => {
310
+ let obj = Dict.fromArray([("field", f->JSON.Encode.string)])
311
+ entry.retiredValues->Option.forEach(vs =>
312
+ obj->Dict.set("values", vs->Array.map(JSON.Encode.string)->JSON.Encode.array)
313
+ )
314
+ dict->Dict.set(tableName, obj->JSON.Encode.object)
315
+ })
316
+ })
317
+ dict->JSON.Encode.object->JSON.stringify
318
+ })
319
+
273
320
  // Shared Lambda — the compiled `_Ops` handler is identical for every
274
321
  // stream-enabled RM; routing is per-record via STATE_TOPIC_MAP env var.
275
322
  // Bundle reventless-aws (the handler + its node:crypto signer + the
@@ -325,6 +372,7 @@ let finish = (
325
372
  ("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make),
326
373
  ("APPSYNC_ENDPOINT", appsyncEndpoint->Pulumi.Output.asInput),
327
374
  ("STATE_TOPIC_MAP", topicMapJson->Pulumi.Output.asInput),
375
+ ("STATE_RETIRED_MAP", retiredMapJson->Pulumi.Output.asInput),
328
376
  ("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
329
377
  ("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
330
378
  Util_LambdaLogging.logLevelEntry(),
@@ -10,6 +10,8 @@ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
10
10
  import * as QueryDb$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb.res.mjs";
11
11
  import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
12
12
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
13
+ import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
14
+ import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
13
15
  import * as Util_ReadModel$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_ReadModel.res.mjs";
14
16
  import * as AppSync_EventsApi$ReventlessAws from "../Api/AppSync_EventsApi.res.mjs";
15
17
  import * as Util_LambdaLogging$ReventlessAws from "../../util/Util_LambdaLogging.res.mjs";
@@ -18,7 +20,7 @@ import * as StateTopic_AppSync_Helpers$ReventlessAws from "./StateTopic_AppSync_
18
20
 
19
21
  let registry = {};
20
22
 
21
- function makeForTable(tableName, streamArn, partitionKeyName, topicName, eventsApi, param) {
23
+ function makeForTable(tableName, streamArn, partitionKeyName, topicName, retiredField, retiredValues, eventsApi, param) {
22
24
  let checkedTableName = Pulumi.all([
23
25
  tableName,
24
26
  partitionKeyName
@@ -32,13 +34,15 @@ function makeForTable(tableName, streamArn, partitionKeyName, topicName, eventsA
32
34
  registry[key] = entries.concat([{
33
35
  tableName: checkedTableName,
34
36
  streamArn: streamArn,
35
- topicName: topicName
37
+ topicName: topicName,
38
+ retiredField: retiredField,
39
+ retiredValues: retiredValues
36
40
  }]);
37
41
  }
38
42
 
39
43
  function make(readModelName, topicName, allQueryDbs, eventsApi, opts) {
40
44
  let streamResource = Util_DynamoDbStream$ReventlessAws.findResource(Util_ReadModel$ReventlessCore.queryDbStorageResources(allQueryDbs, readModelName));
41
- makeForTable(streamResource.name, Util_DynamoDbStream$ReventlessAws.streamArnFromDynamoDbTableResource(streamResource), Pulumi.output(StateTopic_AppSync_Helpers$ReventlessAws.entityKeyPartitionAttribute), topicName, eventsApi, opts);
45
+ makeForTable(streamResource.name, Util_DynamoDbStream$ReventlessAws.streamArnFromDynamoDbTableResource(streamResource), Pulumi.output(StateTopic_AppSync_Helpers$ReventlessAws.entityKeyPartitionAttribute), topicName, Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Plugin_Helpers$ReventlessCore.stateSchemaRegistry[readModelName], StateAnnotations$Reventless.getSpec), spec => spec.retired), r => r.field), Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Plugin_Helpers$ReventlessCore.stateSchemaRegistry[readModelName], StateAnnotations$Reventless.getSpec), spec => spec.retired), r => r.values), eventsApi, opts);
42
46
  }
43
47
 
44
48
  function finish(eventsApi, opts) {
@@ -94,6 +98,23 @@ function finish(eventsApi, opts) {
94
98
  });
95
99
  return JSON.stringify(dict);
96
100
  });
101
+ let retiredMapJson = Pulumi.all(entries.map(e => e.tableName)).apply(tableNames => {
102
+ let dict = {};
103
+ tableNames.forEach((tableName, i) => {
104
+ let entry = entries[i];
105
+ Stdlib_Option.forEach(entry.retiredField, f => {
106
+ let obj = Object.fromEntries([[
107
+ "field",
108
+ f
109
+ ]]);
110
+ Stdlib_Option.forEach(entry.retiredValues, vs => {
111
+ obj["values"] = vs.map(prim => prim);
112
+ });
113
+ dict[tableName] = obj;
114
+ });
115
+ });
116
+ return JSON.stringify(dict);
117
+ });
97
118
  let packageDirs = Object.fromEntries([[
98
119
  "@reventlessdev/reventless-aws",
99
120
  Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws")
@@ -125,6 +146,10 @@ function finish(eventsApi, opts) {
125
146
  "STATE_TOPIC_MAP",
126
147
  topicMapJson
127
148
  ],
149
+ [
150
+ "STATE_RETIRED_MAP",
151
+ retiredMapJson
152
+ ],
128
153
  [
129
154
  "NODE_OPTIONS",
130
155
  Util_Bundle$ReventlessAws.esmLoaderNodeOptions
@@ -53,6 +53,55 @@ type event = {@as("Records") records: array<record>}
53
53
 
54
54
  // ── Per-record derivations (ports of the former inline JS helpers) ───────────
55
55
 
56
+ // STATE_RETIRED_MAP: `{ <tableName>: {field, values?} }`, injected at deploy time
57
+ // beside STATE_TOPIC_MAP. This handler runs with no plugin registry in process —
58
+ // there is nothing here that could read a state schema — so the whole predicate
59
+ // has to arrive the same way the topic routing does. A table absent from the map
60
+ // declares no retirement, which is every table until one does.
61
+ //
62
+ // `values` absent is the boolean form. Carried as an object rather than as an
63
+ // encoded string because a `field=Value` convention is a parser this side and a
64
+ // writer the other, and the two drift the first time a state name contains the
65
+ // separator — an argument that only gets stronger now that a lifecycle can name
66
+ // several states.
67
+ let retiredMap: dict<Reventless.OwnerScope.retiredScope> =
68
+ switch NodeProcess.env
69
+ ->Dict.get("STATE_RETIRED_MAP")
70
+ ->Option.getOr("{}")
71
+ ->JSON.parseOrThrow
72
+ ->JSON.Decode.object {
73
+ | Some(d) =>
74
+ let out = Dict.make()
75
+ d->Dict.forEachWithKey((v, k) =>
76
+ v
77
+ ->JSON.Decode.object
78
+ ->Option.flatMap(o =>
79
+ o
80
+ ->Dict.get("field")
81
+ ->Option.flatMap(JSON.Decode.string)
82
+ ->Option.map(field => {
83
+ Reventless.OwnerScope.field,
84
+ values: o
85
+ ->Dict.get("values")
86
+ ->Option.flatMap(JSON.Decode.array)
87
+ ->Option.map(vs => vs->Array.filterMap(JSON.Decode.string)),
88
+ })
89
+ )
90
+ ->Option.forEach(scope => out->Dict.set(k, scope))
91
+ )
92
+ out
93
+ | None => Dict.make()
94
+ }
95
+
96
+ let tableNameFromEventSourceArn = (arn: string): option<string> => {
97
+ let parts = arn->String.split("/")
98
+ switch (parts->Array.get(0), parts->Array.get(1), parts->Array.get(2)) {
99
+ | (Some(prefix), Some(tableName), Some("stream")) if prefix->String.endsWith(":table") =>
100
+ Some(tableName)
101
+ | _ => None
102
+ }
103
+ }
104
+
56
105
  // Map record.eventSourceARN → topicRoot via STATE_TOPIC_MAP. Stream ARNs look
57
106
  // like arn:aws:dynamodb:<region>:<acct>:table/<TableName>/stream/<ts>; split on
58
107
  // "/" gives [ "...:table", "<TableName>", "stream", "<ts>" ].
@@ -131,18 +180,34 @@ let makeDescriptor = (
131
180
  ~entityKey: string,
132
181
  ~image: dict<JSON.t>,
133
182
  ~seq: option<string>,
183
+ ~retiredField: option<string>=?,
184
+ ~retiredValues: option<array<string>>=?,
134
185
  ): JSON.t => {
135
186
  let removed = changeKind == "Removed"
187
+ // A retired row publishes as metadata only, for the reason the other two
188
+ // implementations do it: this channel reaches every subscriber of the view,
189
+ // and a payload would deliver the row to the callers the resolvers refuse it
190
+ // to. `Updated` with no state is the shape an oversized row already takes.
191
+ // Both forms are the one question `isRetiredValue` answers, so nothing here
192
+ // branches on which the view declared.
193
+ let retired = switch retiredField {
194
+ | Some(field) =>
195
+ !removed &&
196
+ {Reventless.OwnerScope.field, values: retiredValues}->Reventless.OwnerScope.isRetiredValue(
197
+ image->Dict.get(field),
198
+ )
199
+ | None => false
200
+ }
136
201
  let descriptor = Dict.make()
137
202
  descriptor->Dict.set("changeKind", JSON.Encode.string(changeKind))
138
203
  descriptor->Dict.set("id", JSON.Encode.string(entityKey))
139
- if !removed {
204
+ if !removed && !retired {
140
205
  pickSortKeyValue(image)->Option.forEach(v =>
141
206
  descriptor->Dict.set("sortKeyValue", JSON.Encode.string(v))
142
207
  )
143
208
  }
144
209
  seq->Option.forEach(s => descriptor->Dict.set("seq", JSON.Encode.string(s)))
145
- if !removed {
210
+ if !removed && !retired {
146
211
  let state = image->JSON.Encode.object
147
212
  let encoded = state->JSON.stringify
148
213
  if encoded->String.length <= maxStateChars {
@@ -191,6 +256,12 @@ let processRecord = async (
191
256
  ~entityKey,
192
257
  ~image=unmarshalled,
193
258
  ~seq=dynamodb.sequenceNumber,
259
+ ~retiredField=?tableNameFromEventSourceArn(record.eventSourceARN)
260
+ ->Option.flatMap(t => retiredMap->Dict.get(t))
261
+ ->Option.map(scope => scope.Reventless.OwnerScope.field),
262
+ ~retiredValues=?tableNameFromEventSourceArn(record.eventSourceARN)
263
+ ->Option.flatMap(t => retiredMap->Dict.get(t))
264
+ ->Option.flatMap(scope => scope.Reventless.OwnerScope.values),
194
265
  )
195
266
  let body =
196
267
  Dict.fromArray([
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
3
4
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
5
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
6
  import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
@@ -7,6 +8,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
8
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
9
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
9
10
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
11
+ import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
10
12
  import * as DynamoDb_Util_Helpers$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/DynamoDb_Util_Helpers.res.mjs";
11
13
  import * as AppSyncEventsSigner_Ops$ReventlessAws from "../Api/AppSyncEventsSigner_Ops.res.mjs";
12
14
 
@@ -22,6 +24,33 @@ let topicMap = obj !== undefined ? Object.fromEntries(Stdlib_Array.filterMap(Obj
22
24
  ]);
23
25
  })) : ({});
24
26
 
27
+ let d = Stdlib_JSON.Decode.object(JSON.parse(Stdlib_Option.getOr(process.env["STATE_RETIRED_MAP"], "{}")));
28
+
29
+ let retiredMap;
30
+
31
+ if (d !== undefined) {
32
+ let out = {};
33
+ Stdlib_Dict.forEachWithKey(d, (v, k) => Stdlib_Option.forEach(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(v), o => Stdlib_Option.map(Stdlib_Option.flatMap(o["field"], Stdlib_JSON.Decode.string), field => ({
34
+ field: field,
35
+ values: Stdlib_Option.map(Stdlib_Option.flatMap(o["values"], Stdlib_JSON.Decode.array), vs => Stdlib_Array.filterMap(vs, Stdlib_JSON.Decode.string))
36
+ }))), scope => {
37
+ out[k] = scope;
38
+ }));
39
+ retiredMap = out;
40
+ } else {
41
+ retiredMap = {};
42
+ }
43
+
44
+ function tableNameFromEventSourceArn(arn) {
45
+ let parts = arn.split("/");
46
+ let match = parts[0];
47
+ let match$1 = parts[1];
48
+ let match$2 = parts[2];
49
+ if (match !== undefined && match$1 !== undefined && match$2 !== undefined && match$2 === "stream" && match.endsWith(":table")) {
50
+ return match$1;
51
+ }
52
+ }
53
+
25
54
  function topicRootFromEventSourceArn(arn) {
26
55
  let parts = arn.split("/");
27
56
  let match = parts[0];
@@ -89,12 +118,16 @@ function pickSortKeyValue(image) {
89
118
  }
90
119
  }
91
120
 
92
- function makeDescriptor(changeKind, entityKey, image, seq) {
121
+ function makeDescriptor(changeKind, entityKey, image, seq, retiredField, retiredValues) {
93
122
  let removed = changeKind === "Removed";
123
+ let retired = retiredField !== undefined ? !removed && OwnerScope$Reventless.isRetiredValue({
124
+ field: retiredField,
125
+ values: retiredValues
126
+ }, image[retiredField]) : false;
94
127
  let descriptor = {};
95
128
  descriptor["changeKind"] = changeKind;
96
129
  descriptor["id"] = entityKey;
97
- if (!removed) {
130
+ if (!removed && !retired) {
98
131
  Stdlib_Option.forEach(pickSortKeyValue(image), v => {
99
132
  descriptor["sortKeyValue"] = v;
100
133
  });
@@ -102,7 +135,7 @@ function makeDescriptor(changeKind, entityKey, image, seq) {
102
135
  Stdlib_Option.forEach(seq, s => {
103
136
  descriptor["seq"] = s;
104
137
  });
105
- if (!removed) {
138
+ if (!removed && !retired) {
106
139
  let encoded = JSON.stringify(image);
107
140
  if (encoded.length <= 61440) {
108
141
  descriptor["state"] = image;
@@ -127,7 +160,7 @@ async function processRecord(record, region, creds) {
127
160
  let entityKey = entityKeyFromRecord(dynamodb);
128
161
  let channel = `/default/` + topicRoot + `/` + AppSyncEventsSigner_Ops$ReventlessAws.pathSegment(entityKey);
129
162
  let unmarshalled = DynamoDb_Util_Helpers$AwsSdk.unmarshallDict(undefined, image);
130
- let descriptor = makeDescriptor(changeKindFor(record.eventName), entityKey, unmarshalled, dynamodb.SequenceNumber);
163
+ let descriptor = makeDescriptor(changeKindFor(record.eventName), entityKey, unmarshalled, dynamodb.SequenceNumber, Stdlib_Option.map(Stdlib_Option.flatMap(tableNameFromEventSourceArn(record.eventSourceARN), t => retiredMap[t]), scope => scope.field), Stdlib_Option.flatMap(Stdlib_Option.flatMap(tableNameFromEventSourceArn(record.eventSourceARN), t => retiredMap[t]), scope => scope.values));
131
164
  let body = JSON.stringify(Object.fromEntries([
132
165
  [
133
166
  "id",
@@ -193,6 +226,8 @@ let maxStateChars = 61440;
193
226
  export {
194
227
  endpoint,
195
228
  topicMap,
229
+ retiredMap,
230
+ tableNameFromEventSourceArn,
196
231
  topicRootFromEventSourceArn,
197
232
  jsonToString,
198
233
  entityKeyFromRecord,
@@ -52,6 +52,8 @@ let ops: QueryDb_Adapter.operations = {
52
52
  deleteBatch: Obj.magic(0),
53
53
  }
54
54
 
55
+ let lastRetiredScope: ref<option<Reventless.OwnerScope.retiredScope>> = ref(None)
56
+
55
57
  let pushdowns: PgQueryResolver_Lambda.pushdowns = {
56
58
  indexLookup: async (~readModelName as _, field, value) =>
57
59
  allItems()->Array.filter(i => fieldEq(i, field, value)),
@@ -63,11 +65,20 @@ let pushdowns: PgQueryResolver_Lambda.pushdowns = {
63
65
  ~capability as _,
64
66
  ~labelField as _,
65
67
  ~ownerScope: option<(string, string)>=?,
68
+ ~retiredScope: option<Reventless.OwnerScope.retiredScope>=?,
66
69
  ) => {
67
70
  lastOwnerScope := ownerScope
71
+ lastRetiredScope := retiredScope
68
72
  listPageReturn.contents
69
73
  },
70
- itemsPage: async (~readModelName as _, ~subIdField as _, ~id, ~argsDict as _, ~ownerScope as _=?) =>
74
+ itemsPage: async (
75
+ ~readModelName as _,
76
+ ~subIdField as _,
77
+ ~id,
78
+ ~argsDict as _,
79
+ ~ownerScope as _=?,
80
+ ~retiredScope as _=?,
81
+ ) =>
71
82
  // Sentinel echoing the requested id, so the dispatch routing is observable.
72
83
  JSON.Encode.object(Dict.fromArray([("itemsFor", JSON.Encode.string(id))])),
73
84
  scanAll: async (~readModelName as _) => allItems(),
@@ -82,6 +93,8 @@ let makeBinding = (
82
93
  ~authorization=Reventless.Authorization.AllowAnonymous,
83
94
  ~subIdField=None,
84
95
  ~ownerField=None,
96
+ ~retiredField=None,
97
+ ~retiredValues=None,
85
98
  (),
86
99
  ): PgQueryResolver_Lambda.binding => {
87
100
  ops,
@@ -96,6 +109,8 @@ let makeBinding = (
96
109
  includeIdParam: true,
97
110
  authorization,
98
111
  ownerField,
112
+ retiredField,
113
+ retiredValues,
99
114
  }
100
115
 
101
116
  let mkPayload = (