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

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.
@@ -0,0 +1,272 @@
1
+ // The DynamoDB backend narrows retirement on every door the rule names, not
2
+ // only on the list.
3
+ //
4
+ // These assert the *generated resolver source*, which is the only artifact that
5
+ // exists before a deploy: the predicate runs inside AppSync's JS runtime, so a
6
+ // unit test cannot execute it against a table, and what can be checked here is
7
+ // that each door carries the guard, in the half of the template its operation
8
+ // allows, and that a view declaring no retirement is untouched.
9
+
10
+ open JestGlobals
11
+
12
+ module F = AppSync_Resolver_Retrying.Functions
13
+
14
+ // The templates are `Pulumi.Input.t<string>` — a string at rest, wrapped for the
15
+ // provider. Read it back as one so the assertions can be about text.
16
+ external asString: Pulumi.Input.t<string> => string = "%identity"
17
+
18
+ let elevated = ["Admin"]
19
+
20
+ // A product view: retired by two states of its lifecycle, unowned.
21
+ let shelf = (~includeOwner: bool) =>
22
+ F.getItemById(
23
+ ~ownerField=?includeOwner ? Some("customerId") : None,
24
+ ~elevatedGroups=elevated,
25
+ ~retiredField="shelfStatus",
26
+ ~retiredValues=["Archived", "Discontinued"],
27
+ )->asString
28
+
29
+ describe("the single-entity door", () => {
30
+ testSync("withholds a retired row until an exempt caller asks", () => {
31
+ let code = shelf(~includeOwner=false)
32
+ expect(code->String.includes("const _wantsRetired = _exempt && ctx.args.includeRetired === true"))
33
+ ->Expect.toBe(true)
34
+ expect(code->String.includes("_live(ctx.result) ? ctx.result : null"))->Expect.toBe(true)
35
+ })
36
+
37
+ // Absent attribute keeps the row: a row written before the annotation existed
38
+ // is not retired, which is what stops a view emptying the day it lands.
39
+ testSync("reads a missing attribute as not retired", () =>
40
+ expect(shelf(~includeOwner=false)->String.includes("row['shelfStatus'] == null"))->Expect.toBe(true)
41
+ )
42
+
43
+ testSync("tests membership of the retiring states, not equality with one", () =>
44
+ expect(
45
+ shelf(~includeOwner=false)->String.includes(
46
+ "['Archived', 'Discontinued'].indexOf(row['shelfStatus']) >= 0",
47
+ ),
48
+ )->Expect.toBe(true)
49
+ )
50
+
51
+ // Two `const _exempt` in one function body is a syntax error, so the owner
52
+ // preamble's copy is the only one when both guards are present.
53
+ testSync("declares the exemption test once when owner scoping is also on", () => {
54
+ let code = shelf(~includeOwner=true)
55
+ let occurrences =
56
+ code->String.split("const _exempt =")->Array.length - 1
57
+ expect((occurrences, code->String.includes("_owns(ctx.result) && _live(ctx.result)")))
58
+ ->Expect.toEqual((1, true))
59
+ })
60
+
61
+ testSync("is unchanged for a view that declares no retirement", () => {
62
+ let code = F.getItemById(~elevatedGroups=elevated)->asString
63
+ expect((code->String.includes("_wantsRetired"), code->String.includes("includeRetired")))
64
+ ->Expect.toEqual((false, false))
65
+ })
66
+ })
67
+
68
+ describe("the by-ids door", () => {
69
+ let code = F.batchGetItemsByIds(
70
+ ~retiredField="shelfStatus",
71
+ ~retiredValues=["Archived"],
72
+ ~elevatedGroups=elevated,
73
+ )("ProductsTable")
74
+
75
+ // BatchGetItem has no FilterExpression, so the predicate is a filter over what
76
+ // came back — beside the null-drop the non-null list type already needs.
77
+ testSync("drops retired rows from the returned array", () =>
78
+ expect(code->String.includes("item !== null && _live(item)"))->Expect.toBe(true)
79
+ )
80
+
81
+ testSync("still names the table it batches against", () =>
82
+ expect(code->String.includes("ctx.result?.data?.['ProductsTable']"))->Expect.toBe(true)
83
+ )
84
+
85
+ testSync("is unchanged for a view that declares no retirement", () =>
86
+ expect(F.batchGetItemsByIds()("ProductsTable")->String.includes("_live"))
87
+ ->Expect.toBe(false)
88
+ )
89
+ })
90
+
91
+ describe("the by-index door", () => {
92
+ let code = F.queryByIndexFiltered(
93
+ ~index="byCategory",
94
+ ~idField="categoryId",
95
+ ~retiredField="shelfStatus",
96
+ ~retiredValues=["Archived", "Discontinued"],
97
+ ~elevatedGroups=elevated,
98
+ )->asString
99
+
100
+ // A Query takes a FilterExpression, so the predicate is pushed into the read.
101
+ // Narrowing after it would hand back fewer rows than `limit` asked for and say
102
+ // nothing about why.
103
+ testSync("pushes the predicate into the FilterExpression", () => {
104
+ expect(code->String.includes("names['#retired'] = 'shelfStatus'"))->Expect.toBe(true)
105
+ expect(
106
+ code->String.includes(
107
+ "attribute_not_exists(#retired) OR (#retired <> :retiredValue0 AND #retired <> :retiredValue1)",
108
+ ),
109
+ )->Expect.toBe(true)
110
+ })
111
+
112
+ // It composes with the caller's own filter arguments rather than replacing them.
113
+ testSync("ANDs onto an expression the caller may already have built", () =>
114
+ expect(code->String.includes("if (expression) expression += ' AND '"))->Expect.toBe(true)
115
+ )
116
+
117
+ testSync("is unchanged for a view that declares no retirement", () =>
118
+ expect(
119
+ F.queryByIndexFiltered(~index="byCategory", ~idField="categoryId")
120
+ ->asString
121
+ ->String.includes("#retired"),
122
+ )->Expect.toBe(false)
123
+ )
124
+ })
125
+
126
+ describe("the boolean form", () => {
127
+ testSync("tests the flag rather than a set of states", () => {
128
+ let code =
129
+ F.getItemById(~elevatedGroups=elevated, ~retiredField="archived")->asString
130
+ expect((
131
+ code->String.includes("row['archived'] === true"),
132
+ code->String.includes("indexOf(row["),
133
+ ))->Expect.toEqual((true, false))
134
+ })
135
+ })
136
+
137
+ // ── Ownership, on the two doors that never had it ────────────────────────────
138
+
139
+ describe("the by-ids door's owner scoping", () => {
140
+ let code = F.batchGetItemsByIds(
141
+ ~ownerField="customerId",
142
+ ~retiredField="shelfStatus",
143
+ ~retiredValues=["Archived"],
144
+ ~elevatedGroups=elevated,
145
+ )("Orders")
146
+
147
+ // Dropped from the array rather than refused, for the reason the single-key
148
+ // door answers null: telling "not yours" apart from "not there" makes the door
149
+ // an oracle for which ids exist.
150
+ testSync("drops a row the caller does not own", () =>
151
+ expect(code->String.includes("item !== null && _owns(item) && _live(item)"))->Expect.toBe(true)
152
+ )
153
+
154
+ testSync("declares the exemption test once, shared with the retirement guard", () =>
155
+ expect(code->String.split("const _exempt =")->Array.length - 1)->Expect.toBe(1)
156
+ )
157
+
158
+ testSync("is unchanged for a view with neither rule", () => {
159
+ let plain = F.batchGetItemsByIds()("Orders")
160
+ expect((plain->String.includes("_owns"), plain->String.includes("_live")))
161
+ ->Expect.toEqual((false, false))
162
+ })
163
+ })
164
+
165
+ describe("the by-index door's owner scoping", () => {
166
+ let code = F.queryByIndexFiltered(
167
+ ~index="byCategory",
168
+ ~idField="categoryId",
169
+ ~ownerField="customerId",
170
+ ~elevatedGroups=elevated,
171
+ )->asString
172
+
173
+ testSync("pushes the owner predicate into the read", () =>
174
+ expect(code->String.includes("names['#owner'] = 'customerId'"))->Expect.toBe(true)
175
+ )
176
+
177
+ // The predicate must not be readable off the arguments: a rule about what the
178
+ // caller may see cannot arrive on a channel the caller controls.
179
+ testSync("reads the owner from the identity, never from ctx.args", () =>
180
+ expect(code->String.includes("values[':owner'] = _osub"))->Expect.toBe(true)
181
+ )
182
+
183
+ testSync("exempts an elevated caller and an IAM service call", () =>
184
+ expect(
185
+ code->String.includes("if (!(_osub == null || _ogroups.some(g => _oelevated.indexOf(g) >= 0)))"),
186
+ )->Expect.toBe(true)
187
+ )
188
+
189
+ // The generic argument loop turns an unrecognised arg into a `contains` filter.
190
+ // `includeRetired` is a request to lift a restriction, and filtering on it
191
+ // would match no row at all.
192
+ testSync("never treats includeRetired as a column to match on", () =>
193
+ expect(code->String.includes("key === 'includeRetired'"))->Expect.toBe(true)
194
+ )
195
+ })
196
+
197
+ // The by-index door was, until this landed, unreachable rather than merely
198
+ // un-widenable: the SDL declared `id` while the template below read the index
199
+ // key, and the template answered with DynamoDB's `{items, nextToken}` against a
200
+ // field that promised a Connection. These assert the two halves that were wrong,
201
+ // beside the paging arguments the field actually offers.
202
+ describe("the by-index door answers the field it is attached to", () => {
203
+ let code =
204
+ F.queryByIndexFiltered(~index="byCategory", ~idField="categoryId")->asString
205
+
206
+ testSync("keys the read on the index column the SDL offers", () =>
207
+ expect(code->String.includes("util.dynamodb.toDynamoDB(args.categoryId)"))->Expect.toBe(true)
208
+ )
209
+
210
+ testSync("returns a Relay connection, not the raw DynamoDB result", () =>
211
+ expect((
212
+ code->String.includes("edges,"),
213
+ code->String.includes("hasNextPage: !!next"),
214
+ code->String.includes("return ctx.result;"),
215
+ ))->Expect.toEqual((true, true, false))
216
+ )
217
+
218
+ // `first`/`after` are what the field declares; `limit`/`nextToken` were what
219
+ // the template read, and nothing translated between the two.
220
+ testSync("pages on the Relay arguments the field declares", () =>
221
+ expect((
222
+ code->String.includes("limit: (args.first ?? 50)"),
223
+ code->String.includes("util.base64Decode(args.after)"),
224
+ ))->Expect.toEqual((true, true))
225
+ )
226
+
227
+ // Every declared argument has a job other than matching a column, so each one
228
+ // reaching the generic filter loop would filter on an attribute no row has.
229
+ testSync("keeps every paging argument out of the filter loop", () =>
230
+ expect(
231
+ ["first", "after", "last", "before", "includeRetired"]->Array.every(arg =>
232
+ code->String.includes(`key === '${arg}'`)
233
+ ),
234
+ )->Expect.toBe(true)
235
+ )
236
+ })
237
+
238
+ // Backward paging is refused rather than ignored. The cursor is DynamoDB's own
239
+ // continuation token, which walks one way, so `last: 2` could only ever be
240
+ // answered with the first two rows — a different question, answered silently.
241
+ // `listAllItemsConnection` set this rule; the local backend refuses with the
242
+ // same sentence, so the door reads the same either side of a deploy.
243
+ describe("the by-index door's backward paging", () => {
244
+ let refusal = "Backward pagination (last/before) is not supported on by-index connections"
245
+
246
+ testSync("refuses last/before on the plain index read", () =>
247
+ expect(
248
+ F.queryByIndexFiltered(~index="byCategory", ~idField="categoryId")
249
+ ->asString
250
+ ->String.includes(refusal),
251
+ )->Expect.toBe(true)
252
+ )
253
+
254
+ testSync("refuses them on the sort-key index read too", () =>
255
+ expect(
256
+ F.queryByIndexSortFiltered(~index="byCustomer", ~idField="customerId", ~sortField="placedAt")
257
+ ->asString
258
+ ->String.includes(refusal),
259
+ )->Expect.toBe(true)
260
+ )
261
+
262
+ // Before the key condition is built, so a refused request never reaches the
263
+ // read — and `util.error` is what AppSync returns to the caller as the field
264
+ // error, rather than an empty page that looks like an answer.
265
+ testSync("refuses before doing any work, and says so to the caller", () => {
266
+ let code = F.queryByIndexFiltered(~index="byCategory", ~idField="categoryId")->asString
267
+ let guardAt = code->String.indexOf("args.before != null")
268
+ let queryAt = code->String.indexOf("expressionValues")
269
+ expect((guardAt >= 0 && guardAt < queryAt, code->String.includes("'UnsupportedPagination'")))
270
+ ->Expect.toEqual((true, true))
271
+ })
272
+ })
@@ -0,0 +1,193 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as AppSync_Resolver_Functions$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/AppSync/AppSync_Resolver_Functions.res.mjs";
4
+
5
+ let elevated = ["Admin"];
6
+
7
+ function shelf(includeOwner) {
8
+ return AppSync_Resolver_Functions$PulumiAws.getItemById(includeOwner ? "customerId" : undefined, elevated, "shelfStatus", [
9
+ "Archived",
10
+ "Discontinued"
11
+ ]);
12
+ }
13
+
14
+ globalThis.describe("the single-entity door", () => {
15
+ globalThis.test("withholds a retired row until an exempt caller asks", () => {
16
+ let code = shelf(false);
17
+ globalThis.expect(code.includes("const _wantsRetired = _exempt && ctx.args.includeRetired === true")).toBe(true);
18
+ globalThis.expect(code.includes("_live(ctx.result) ? ctx.result : null")).toBe(true);
19
+ });
20
+ globalThis.test("reads a missing attribute as not retired", () => {
21
+ globalThis.expect(shelf(false).includes("row['shelfStatus'] == null")).toBe(true);
22
+ });
23
+ globalThis.test("tests membership of the retiring states, not equality with one", () => {
24
+ globalThis.expect(shelf(false).includes("['Archived', 'Discontinued'].indexOf(row['shelfStatus']) >= 0")).toBe(true);
25
+ });
26
+ globalThis.test("declares the exemption test once when owner scoping is also on", () => {
27
+ let code = shelf(true);
28
+ let occurrences = code.split("const _exempt =").length - 1 | 0;
29
+ globalThis.expect([
30
+ occurrences,
31
+ code.includes("_owns(ctx.result) && _live(ctx.result)")
32
+ ]).toEqual([
33
+ 1,
34
+ true
35
+ ]);
36
+ });
37
+ globalThis.test("is unchanged for a view that declares no retirement", () => {
38
+ let code = AppSync_Resolver_Functions$PulumiAws.getItemById(undefined, elevated, undefined, undefined);
39
+ globalThis.expect([
40
+ code.includes("_wantsRetired"),
41
+ code.includes("includeRetired")
42
+ ]).toEqual([
43
+ false,
44
+ false
45
+ ]);
46
+ });
47
+ });
48
+
49
+ globalThis.describe("the by-ids door", () => {
50
+ let code = AppSync_Resolver_Functions$PulumiAws.batchGetItemsByIds(undefined, "shelfStatus", ["Archived"], elevated)("ProductsTable");
51
+ globalThis.test("drops retired rows from the returned array", () => {
52
+ globalThis.expect(code.includes("item !== null && _live(item)")).toBe(true);
53
+ });
54
+ globalThis.test("still names the table it batches against", () => {
55
+ globalThis.expect(code.includes("ctx.result?.data?.['ProductsTable']")).toBe(true);
56
+ });
57
+ globalThis.test("is unchanged for a view that declares no retirement", () => {
58
+ globalThis.expect(AppSync_Resolver_Functions$PulumiAws.batchGetItemsByIds(undefined, undefined, undefined, undefined)("ProductsTable").includes("_live")).toBe(false);
59
+ });
60
+ });
61
+
62
+ globalThis.describe("the by-index door", () => {
63
+ let code = AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered("byCategory", "categoryId", undefined, "shelfStatus", [
64
+ "Archived",
65
+ "Discontinued"
66
+ ], elevated);
67
+ globalThis.test("pushes the predicate into the FilterExpression", () => {
68
+ globalThis.expect(code.includes("names['#retired'] = 'shelfStatus'")).toBe(true);
69
+ globalThis.expect(code.includes("attribute_not_exists(#retired) OR (#retired <> :retiredValue0 AND #retired <> :retiredValue1)")).toBe(true);
70
+ });
71
+ globalThis.test("ANDs onto an expression the caller may already have built", () => {
72
+ globalThis.expect(code.includes("if (expression) expression += ' AND '")).toBe(true);
73
+ });
74
+ globalThis.test("is unchanged for a view that declares no retirement", () => {
75
+ globalThis.expect(AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered("byCategory", "categoryId", undefined, undefined, undefined, undefined).includes("#retired")).toBe(false);
76
+ });
77
+ });
78
+
79
+ globalThis.describe("the boolean form", () => {
80
+ globalThis.test("tests the flag rather than a set of states", () => {
81
+ let code = AppSync_Resolver_Functions$PulumiAws.getItemById(undefined, elevated, "archived", undefined);
82
+ globalThis.expect([
83
+ code.includes("row['archived'] === true"),
84
+ code.includes("indexOf(row[")
85
+ ]).toEqual([
86
+ true,
87
+ false
88
+ ]);
89
+ });
90
+ });
91
+
92
+ globalThis.describe("the by-ids door's owner scoping", () => {
93
+ let code = AppSync_Resolver_Functions$PulumiAws.batchGetItemsByIds("customerId", "shelfStatus", ["Archived"], elevated)("Orders");
94
+ globalThis.test("drops a row the caller does not own", () => {
95
+ globalThis.expect(code.includes("item !== null && _owns(item) && _live(item)")).toBe(true);
96
+ });
97
+ globalThis.test("declares the exemption test once, shared with the retirement guard", () => {
98
+ globalThis.expect(code.split("const _exempt =").length - 1 | 0).toBe(1);
99
+ });
100
+ globalThis.test("is unchanged for a view with neither rule", () => {
101
+ let plain = AppSync_Resolver_Functions$PulumiAws.batchGetItemsByIds(undefined, undefined, undefined, undefined)("Orders");
102
+ globalThis.expect([
103
+ plain.includes("_owns"),
104
+ plain.includes("_live")
105
+ ]).toEqual([
106
+ false,
107
+ false
108
+ ]);
109
+ });
110
+ });
111
+
112
+ globalThis.describe("the by-index door's owner scoping", () => {
113
+ let code = AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered("byCategory", "categoryId", "customerId", undefined, undefined, elevated);
114
+ globalThis.test("pushes the owner predicate into the read", () => {
115
+ globalThis.expect(code.includes("names['#owner'] = 'customerId'")).toBe(true);
116
+ });
117
+ globalThis.test("reads the owner from the identity, never from ctx.args", () => {
118
+ globalThis.expect(code.includes("values[':owner'] = _osub")).toBe(true);
119
+ });
120
+ globalThis.test("exempts an elevated caller and an IAM service call", () => {
121
+ globalThis.expect(code.includes("if (!(_osub == null || _ogroups.some(g => _oelevated.indexOf(g) >= 0)))")).toBe(true);
122
+ });
123
+ globalThis.test("never treats includeRetired as a column to match on", () => {
124
+ globalThis.expect(code.includes("key === 'includeRetired'")).toBe(true);
125
+ });
126
+ });
127
+
128
+ globalThis.describe("the by-index door answers the field it is attached to", () => {
129
+ let code = AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered("byCategory", "categoryId", undefined, undefined, undefined, undefined);
130
+ globalThis.test("keys the read on the index column the SDL offers", () => {
131
+ globalThis.expect(code.includes("util.dynamodb.toDynamoDB(args.categoryId)")).toBe(true);
132
+ });
133
+ globalThis.test("returns a Relay connection, not the raw DynamoDB result", () => {
134
+ globalThis.expect([
135
+ code.includes("edges,"),
136
+ code.includes("hasNextPage: !!next"),
137
+ code.includes("return ctx.result;")
138
+ ]).toEqual([
139
+ true,
140
+ true,
141
+ false
142
+ ]);
143
+ });
144
+ globalThis.test("pages on the Relay arguments the field declares", () => {
145
+ globalThis.expect([
146
+ code.includes("limit: (args.first ?? 50)"),
147
+ code.includes("util.base64Decode(args.after)")
148
+ ]).toEqual([
149
+ true,
150
+ true
151
+ ]);
152
+ });
153
+ globalThis.test("keeps every paging argument out of the filter loop", () => {
154
+ globalThis.expect([
155
+ "first",
156
+ "after",
157
+ "last",
158
+ "before",
159
+ "includeRetired"
160
+ ].every(arg => code.includes(`key === '` + arg + `'`))).toBe(true);
161
+ });
162
+ });
163
+
164
+ globalThis.describe("the by-index door's backward paging", () => {
165
+ let refusal = "Backward pagination (last/before) is not supported on by-index connections";
166
+ globalThis.test("refuses last/before on the plain index read", () => {
167
+ globalThis.expect(AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered("byCategory", "categoryId", undefined, undefined, undefined, undefined).includes(refusal)).toBe(true);
168
+ });
169
+ globalThis.test("refuses them on the sort-key index read too", () => {
170
+ globalThis.expect(AppSync_Resolver_Functions$PulumiAws.queryByIndexSortFiltered("byCustomer", "customerId", undefined, "placedAt", undefined, undefined, undefined).includes(refusal)).toBe(true);
171
+ });
172
+ globalThis.test("refuses before doing any work, and says so to the caller", () => {
173
+ let code = AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered("byCategory", "categoryId", undefined, undefined, undefined, undefined);
174
+ let guardAt = code.indexOf("args.before != null");
175
+ let queryAt = code.indexOf("expressionValues");
176
+ globalThis.expect([
177
+ guardAt >= 0 && guardAt < queryAt,
178
+ code.includes("'UnsupportedPagination'")
179
+ ]).toEqual([
180
+ true,
181
+ true
182
+ ]);
183
+ });
184
+ });
185
+
186
+ let F;
187
+
188
+ export {
189
+ F,
190
+ elevated,
191
+ shelf,
192
+ }
193
+ /* Not a pure module */
@@ -95,6 +95,7 @@ let makeBinding = (
95
95
  ~ownerField=None,
96
96
  ~retiredField=None,
97
97
  ~retiredValues=None,
98
+ ~namedWhenRetired=false,
98
99
  (),
99
100
  ): PgQueryResolver_Lambda.binding => {
100
101
  ops,
@@ -111,6 +112,7 @@ let makeBinding = (
111
112
  ownerField,
112
113
  retiredField,
113
114
  retiredValues,
115
+ namedWhenRetired,
114
116
  }
115
117
 
116
118
  let mkPayload = (