@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 +19 -0
- package/package.json +11 -11
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +64 -0
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +54 -2
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +111 -33
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +20 -17
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +19 -11
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +11 -10
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +6 -0
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +2 -1
- package/tests/AppSync_RetirementNarrowingTest.res +310 -0
- package/tests/AppSync_RetirementNarrowingTest.res.mjs +216 -0
- package/tests/PgQueryResolver_LambdaTest.res +2 -0
- package/tests/PgQueryResolver_LambdaTest.res.mjs +34 -30
|
@@ -0,0 +1,310 @@
|
|
|
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
|
+
})
|
|
273
|
+
|
|
274
|
+
// APPSYNC_JS is type-checked, so the emitted source has to type-check as well as
|
|
275
|
+
// read correctly. The stub a door emits when a view declares no `@owner` is
|
|
276
|
+
// still *called* with the row on the doors that narrow retirement, and a
|
|
277
|
+
// zero-parameter `() => true` makes that call TS2554 — which AppSync reports at
|
|
278
|
+
// create time as "The code contains one or more errors", failing the deploy on
|
|
279
|
+
// a resolver whose text looks perfectly reasonable.
|
|
280
|
+
describe("the unowned stub is callable where the door calls it", () => {
|
|
281
|
+
// Retirement without an owner is the combination that emits the stub and then
|
|
282
|
+
// calls it: with neither rule the guard is not emitted at all, and with an
|
|
283
|
+
// owner the real one-parameter test replaces it.
|
|
284
|
+
testSync("declares a parameter on the single-entity door", () => {
|
|
285
|
+
let code =
|
|
286
|
+
F.ownerScopedResultResponse(
|
|
287
|
+
~ownerField=None,
|
|
288
|
+
~elevatedGroups=elevated,
|
|
289
|
+
~retiredField="lifecycle",
|
|
290
|
+
~retiredValues=["Archived"],
|
|
291
|
+
)
|
|
292
|
+
expect((
|
|
293
|
+
code->String.includes("const _owns = (row) => true;"),
|
|
294
|
+
code->String.includes("const _owns = () => true;"),
|
|
295
|
+
))->Expect.toEqual((true, false))
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
testSync("declares a parameter on the reference door", () => {
|
|
299
|
+
let code = F.refsByIds(
|
|
300
|
+
~labelField="name",
|
|
301
|
+
~retiredField=None,
|
|
302
|
+
~retiredValues=None,
|
|
303
|
+
~namedWhenRetired=false,
|
|
304
|
+
)("Products")
|
|
305
|
+
expect((
|
|
306
|
+
code->String.includes("const _owns = (row) => true;"),
|
|
307
|
+
code->String.includes("_owns(row)"),
|
|
308
|
+
))->Expect.toEqual((true, true))
|
|
309
|
+
})
|
|
310
|
+
})
|
|
@@ -0,0 +1,216 @@
|
|
|
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
|
+
globalThis.describe("the unowned stub is callable where the door calls it", () => {
|
|
187
|
+
globalThis.test("declares a parameter on the single-entity door", () => {
|
|
188
|
+
let code = AppSync_Resolver_Functions$PulumiAws.ownerScopedResultResponse(undefined, elevated, "lifecycle", ["Archived"]);
|
|
189
|
+
globalThis.expect([
|
|
190
|
+
code.includes("const _owns = (row) => true;"),
|
|
191
|
+
code.includes("const _owns = () => true;")
|
|
192
|
+
]).toEqual([
|
|
193
|
+
true,
|
|
194
|
+
false
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
globalThis.test("declares a parameter on the reference door", () => {
|
|
198
|
+
let code = AppSync_Resolver_Functions$PulumiAws.refsByIds("name", undefined, undefined, false, undefined, undefined)("Products");
|
|
199
|
+
globalThis.expect([
|
|
200
|
+
code.includes("const _owns = (row) => true;"),
|
|
201
|
+
code.includes("_owns(row)")
|
|
202
|
+
]).toEqual([
|
|
203
|
+
true,
|
|
204
|
+
true
|
|
205
|
+
]);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
let F;
|
|
210
|
+
|
|
211
|
+
export {
|
|
212
|
+
F,
|
|
213
|
+
elevated,
|
|
214
|
+
shelf,
|
|
215
|
+
}
|
|
216
|
+
/* 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 = (
|