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

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.
@@ -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 = (
@@ -68,6 +68,10 @@ let ops = {
68
68
  deleteBatch: 0
69
69
  };
70
70
 
71
+ let lastRetiredScope = {
72
+ contents: undefined
73
+ };
74
+
71
75
  async function pushdowns_indexLookup(param, field, value) {
72
76
  return Object.values(store).filter(i => fieldEq(i, field, value));
73
77
  }
@@ -76,12 +80,13 @@ async function pushdowns_byIds(param, ids) {
76
80
  return Stdlib_Array.filterMap(ids, id => store[id]);
77
81
  }
78
82
 
79
- async function pushdowns_listPage(param, param$1, param$2, param$3, ownerScope) {
83
+ async function pushdowns_listPage(param, param$1, param$2, param$3, ownerScope, retiredScope) {
80
84
  lastOwnerScope.contents = ownerScope;
85
+ lastRetiredScope.contents = retiredScope;
81
86
  return listPageReturn.contents;
82
87
  }
83
88
 
84
- async function pushdowns_itemsPage(param, param$1, id, param$2, param$3) {
89
+ async function pushdowns_itemsPage(param, param$1, id, param$2, param$3, param$4) {
85
90
  return Object.fromEntries([[
86
91
  "itemsFor",
87
92
  id
@@ -116,10 +121,12 @@ let capability = {
116
121
  sortFields: capability_sortFields
117
122
  };
118
123
 
119
- function makeBinding(authorizationOpt, subIdFieldOpt, ownerFieldOpt, param) {
124
+ function makeBinding(authorizationOpt, subIdFieldOpt, ownerFieldOpt, retiredFieldOpt, retiredValuesOpt, param) {
120
125
  let authorization = authorizationOpt !== undefined ? authorizationOpt : "AllowAnonymous";
121
126
  let subIdField = subIdFieldOpt !== undefined ? Primitive_option.valFromOption(subIdFieldOpt) : undefined;
122
127
  let ownerField = ownerFieldOpt !== undefined ? Primitive_option.valFromOption(ownerFieldOpt) : undefined;
128
+ let retiredField = retiredFieldOpt !== undefined ? Primitive_option.valFromOption(retiredFieldOpt) : undefined;
129
+ let retiredValues = retiredValuesOpt !== undefined ? Primitive_option.valFromOption(retiredValuesOpt) : undefined;
123
130
  return {
124
131
  ops: ops,
125
132
  pushdowns: pushdowns,
@@ -134,7 +141,9 @@ function makeBinding(authorizationOpt, subIdFieldOpt, ownerFieldOpt, param) {
134
141
  labelField: "name",
135
142
  includeIdParam: true,
136
143
  authorization: authorization,
137
- ownerField: ownerField
144
+ ownerField: ownerField,
145
+ retiredField: retiredField,
146
+ retiredValues: retiredValues
138
147
  };
139
148
  }
140
149
 
@@ -177,7 +186,7 @@ function ids(arr) {
177
186
 
178
187
  globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
179
188
  globalThis.test("getById returns the item (id-carrying)", async () => {
180
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
189
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
181
190
  "id",
182
191
  "p-2"
183
192
  ]]), undefined, undefined));
@@ -185,14 +194,14 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
185
194
  globalThis.expect(str(r, "name")).toEqual("Bravo");
186
195
  });
187
196
  globalThis.test("getById miss → null", async () => {
188
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
197
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
189
198
  "id",
190
199
  "absent"
191
200
  ]]), undefined, undefined));
192
201
  globalThis.expect(r).toBe(null);
193
202
  });
194
203
  globalThis.test("byIds returns matches, drops missing", async () => {
195
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("byIds", undefined, Object.fromEntries([[
204
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("byIds", undefined, Object.fromEntries([[
196
205
  "ids",
197
206
  [
198
207
  "p-1",
@@ -206,7 +215,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
206
215
  ]);
207
216
  });
208
217
  globalThis.test("index routes to indexLookup on the idField", async () => {
209
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("index", "byStatus", Object.fromEntries([[
218
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("index", "byStatus", Object.fromEntries([[
210
219
  "byStatus",
211
220
  "active"
212
221
  ]]), undefined, undefined));
@@ -217,13 +226,13 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
217
226
  });
218
227
  globalThis.test("list with push-down → returns the connection listPage gives", async () => {
219
228
  listPageReturn.contents = "PUSHED";
220
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("list", undefined, undefined, undefined, undefined));
229
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("list", undefined, undefined, undefined, undefined));
221
230
  listPageReturn.contents = undefined;
222
231
  globalThis.expect(r).toEqual("PUSHED");
223
232
  });
224
233
  globalThis.test("list without push-down → falls back to the shared spec", async () => {
225
234
  listPageReturn.contents = undefined;
226
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("list", undefined, undefined, undefined, undefined));
235
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("list", undefined, undefined, undefined, undefined));
227
236
  let edgeIds = Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(field(r, "edges"), Stdlib_JSON.Decode.array), []), e => Stdlib_Option.flatMap(field(e, "node"), n => str(n, "id")));
228
237
  globalThis.expect(edgeIds).toEqual([
229
238
  "p-1",
@@ -232,21 +241,21 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
232
241
  ]);
233
242
  });
234
243
  globalThis.test("items with subId routes to itemsPage (id echoed)", async () => {
235
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, "seq", undefined, undefined), undefined, mkPayload("items", undefined, Object.fromEntries([[
244
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, "seq", undefined, undefined, undefined, undefined), undefined, mkPayload("items", undefined, Object.fromEntries([[
236
245
  "id",
237
246
  "p-2"
238
247
  ]]), undefined, undefined));
239
248
  globalThis.expect(str(r, "itemsFor")).toEqual("p-2");
240
249
  });
241
250
  globalThis.test("items without subId → empty connection", async () => {
242
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("items", undefined, Object.fromEntries([[
251
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("items", undefined, Object.fromEntries([[
243
252
  "id",
244
253
  "p-2"
245
254
  ]]), undefined, undefined));
246
255
  globalThis.expect(Stdlib_Option.getOr(Stdlib_Option.flatMap(field(r, "edges"), Stdlib_JSON.Decode.array), []).length).toBe(0);
247
256
  });
248
257
  globalThis.test("authorization DenyAll → empty shape per kind", async () => {
249
- let b = makeBinding("DenyAll", undefined, undefined, undefined);
258
+ let b = makeBinding("DenyAll", undefined, undefined, undefined, undefined, undefined);
250
259
  let single = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, undefined, mkPayload("getById", undefined, Object.fromEntries([[
251
260
  "id",
252
261
  "p-1"
@@ -265,7 +274,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
265
274
  TAG: "Deny",
266
275
  _0: "nope"
267
276
  }));
268
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
277
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
269
278
  "id",
270
279
  "p-1"
271
280
  ]]), undefined, undefined));
@@ -291,7 +300,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
291
300
  arguments: payload_arguments,
292
301
  identity: Identity$Reventless.anonymous
293
302
  };
294
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, payload);
303
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, payload);
295
304
  globalThis.expect(str(r, "id")).toEqual("p-2");
296
305
  });
297
306
  globalThis.test("resolveOne multi via target index → array", async () => {
@@ -317,7 +326,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
317
326
  arguments: payload_arguments,
318
327
  identity: Identity$Reventless.anonymous
319
328
  };
320
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, payload);
329
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, payload);
321
330
  globalThis.expect(ids(r)).toEqual([
322
331
  "p-1",
323
332
  "p-3"
@@ -344,14 +353,14 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
344
353
  arguments: payload_arguments,
345
354
  identity: Identity$Reventless.anonymous
346
355
  };
347
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, payload);
356
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, payload);
348
357
  globalThis.expect(ids(r)).toEqual([
349
358
  "p-1",
350
359
  "p-3"
351
360
  ]);
352
361
  });
353
362
  globalThis.test("node decodes global id → __typename + item", async () => {
354
- PgQueryResolver_Lambda$ReventlessAws.register("Things", makeBinding(undefined, undefined, undefined, undefined));
363
+ PgQueryResolver_Lambda$ReventlessAws.register("Things", makeBinding(undefined, undefined, undefined, undefined, undefined, undefined));
355
364
  PgQueryResolver_Lambda$ReventlessAws.registerNodeType("Thing", "Things");
356
365
  let payload_arguments = Object.fromEntries([[
357
366
  "id",
@@ -401,7 +410,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
401
410
  delete: 0,
402
411
  deleteBatch: 0
403
412
  };
404
- let init = makeBinding(undefined, undefined, undefined, undefined);
413
+ let init = makeBinding(undefined, undefined, undefined, undefined, undefined, undefined);
405
414
  let authBinding_pushdowns = init.pushdowns;
406
415
  let authBinding_indexes = init.indexes;
407
416
  let authBinding_subIdField = init.subIdField;
@@ -410,6 +419,8 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
410
419
  let authBinding_includeIdParam = init.includeIdParam;
411
420
  let authBinding_authorization = init.authorization;
412
421
  let authBinding_ownerField = init.ownerField;
422
+ let authBinding_retiredField = init.retiredField;
423
+ let authBinding_retiredValues = init.retiredValues;
413
424
  let authBinding = {
414
425
  ops: authOps,
415
426
  pushdowns: authBinding_pushdowns,
@@ -419,7 +430,9 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
419
430
  labelField: authBinding_labelField,
420
431
  includeIdParam: authBinding_includeIdParam,
421
432
  authorization: authBinding_authorization,
422
- ownerField: authBinding_ownerField
433
+ ownerField: authBinding_ownerField,
434
+ retiredField: authBinding_retiredField,
435
+ retiredValues: authBinding_retiredValues
423
436
  };
424
437
  let lookupAuth = name => {
425
438
  if (name === "AuthTable") {
@@ -445,18 +458,18 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
445
458
  identity: ident(username, groups)
446
459
  });
447
460
  globalThis.test("auth index: group member who owns → results", async () => {
448
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), lookupAuth, authIndexPayload("alice", ["Owner"]));
461
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), lookupAuth, authIndexPayload("alice", ["Owner"]));
449
462
  globalThis.expect(ids(r)).toEqual([
450
463
  "p-1",
451
464
  "p-3"
452
465
  ]);
453
466
  });
454
467
  globalThis.test("auth index: group member who does NOT own → empty", async () => {
455
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), lookupAuth, authIndexPayload("bob", ["Owner"]));
468
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), lookupAuth, authIndexPayload("bob", ["Owner"]));
456
469
  globalThis.expect(Stdlib_Option.getOr(Stdlib_JSON.Decode.array(r), []).length).toBe(0);
457
470
  });
458
471
  globalThis.test("auth index: non-member passes through → results", async () => {
459
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), lookupAuth, authIndexPayload("bob", []));
472
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), lookupAuth, authIndexPayload("bob", []));
460
473
  globalThis.expect(ids(r)).toEqual([
461
474
  "p-1",
462
475
  "p-3"
@@ -465,7 +478,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
465
478
  globalThis.test("unmapped kind throws", async () => {
466
479
  let threw;
467
480
  try {
468
- await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("frobnicate", undefined, undefined, undefined, undefined));
481
+ await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("frobnicate", undefined, undefined, undefined, undefined));
469
482
  threw = false;
470
483
  } catch (exn) {
471
484
  threw = true;
@@ -476,35 +489,35 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
476
489
 
477
490
  globalThis.describe("id form — the typed doors take either", () => {
478
491
  globalThis.test("getById resolves a Relay global id", async () => {
479
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
492
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
480
493
  "id",
481
494
  Api_Ids$ReventlessCore.encode("Thing", "p-2")
482
495
  ]]), undefined, undefined));
483
496
  globalThis.expect(str(r, "name")).toEqual("Bravo");
484
497
  });
485
498
  globalThis.test("getById reports the storage key it resolved to", async () => {
486
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
499
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
487
500
  "id",
488
501
  Api_Ids$ReventlessCore.encode("Thing", "p-2")
489
502
  ]]), undefined, undefined));
490
503
  globalThis.expect(str(r, "id")).toEqual("p-2");
491
504
  });
492
505
  globalThis.test("getById still resolves a plain storage key", async () => {
493
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
506
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
494
507
  "id",
495
508
  "p-2"
496
509
  ]]), undefined, undefined));
497
510
  globalThis.expect(str(r, "name")).toEqual("Bravo");
498
511
  });
499
512
  globalThis.test("getById on a global id for a row that does not exist is still null", async () => {
500
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
513
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
501
514
  "id",
502
515
  Api_Ids$ReventlessCore.encode("Thing", "absent")
503
516
  ]]), undefined, undefined));
504
517
  globalThis.expect(r).toBe(null);
505
518
  });
506
519
  globalThis.test("byIds accepts the two forms mixed in one call", async () => {
507
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("byIds", undefined, Object.fromEntries([[
520
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("byIds", undefined, Object.fromEntries([[
508
521
  "ids",
509
522
  [
510
523
  "p-1",
@@ -517,7 +530,7 @@ globalThis.describe("id form — the typed doors take either", () => {
517
530
  ]);
518
531
  });
519
532
  globalThis.test("byIds with every id found does not double-count", async () => {
520
- let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined), undefined, mkPayload("byIds", undefined, Object.fromEntries([[
533
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("byIds", undefined, Object.fromEntries([[
521
534
  "ids",
522
535
  [
523
536
  "p-1",
@@ -532,7 +545,7 @@ globalThis.describe("id form — the typed doors take either", () => {
532
545
  });
533
546
 
534
547
  globalThis.describe("owner scoping", () => {
535
- let ownedBinding = () => makeBinding(undefined, undefined, "owner", undefined);
548
+ let ownedBinding = () => makeBinding(undefined, undefined, "owner", undefined, undefined, undefined);
536
549
  let shopper = cognito("u-a", ["User"]);
537
550
  let listWith = async (binding, identity) => {
538
551
  lastOwnerScope.contents = undefined;
@@ -557,7 +570,7 @@ globalThis.describe("owner scoping", () => {
557
570
  globalThis.expect(match[1]).toBe(undefined);
558
571
  });
559
572
  globalThis.test("a view with no owner field is never scoped", async () => {
560
- let match = await listWith(makeBinding(undefined, undefined, undefined, undefined), shopper);
573
+ let match = await listWith(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined), shopper);
561
574
  globalThis.expect(match[1]).toBe(undefined);
562
575
  });
563
576
  globalThis.test("an unidentified caller is refused before the push-down runs", async () => {
@@ -620,6 +633,7 @@ export {
620
633
  listPageReturn,
621
634
  lastOwnerScope,
622
635
  ops,
636
+ lastRetiredScope,
623
637
  pushdowns,
624
638
  capability,
625
639
  makeBinding,