@reventlessdev/rescript-pulumi-aws 3.0.0-alpha.3 → 3.0.0-alpha.31
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 +232 -0
- package/package.json +8 -7
- package/rescript.json +2 -1
- package/src/AppSync/AppSync_ResolverRuntime.res +4 -5
- package/src/AppSync/AppSync_Resolver_Functions.res +524 -396
- package/src/AppSync/AppSync_Resolver_Functions.res.mjs +206 -130
- package/src/AppSync/AppSync_SourceApiAssociation.res +1 -3
- package/src/AwsNative/AppSync/AwsNative_AppSync_Api.res +2 -3
- package/src/AwsNative/AppSync/AwsNative_AppSync_ChannelNamespace.res +2 -6
- package/src/AwsNative/AppSync/AwsNative_AppSync_Resolver.res +2 -5
- package/src/Cloudwatch/Cloudwatch_EventRule.res +4 -0
- package/src/Cloudwatch/Cloudwatch_EventTarget.res +11 -0
- package/src/Cloudwatch/Cloudwatch_LogMetricFilter.res +0 -1
- package/src/Cloudwatch/Cloudwatch_MetricAlarm.res +1 -2
- package/src/Cognito/Cognito_UserPool.res +2 -4
- package/src/IAM/IAM.res +1 -1
- package/src/IAM/PolicyDocument.res +2 -2
- package/src/IAM/PolicyDocument.res.mjs +3 -3
- package/src/Kinesis/Kinesis_Stream.res +3 -6
- package/src/Lambda/FunctionUrl.res +5 -2
- package/src/Lambda/Lambda.res +138 -52
- package/src/Lambda/Lambda.res.mjs +152 -23
- package/src/Rds/Rds_Cluster.res +0 -1
- package/src/Rds/Rds_Instance.res +0 -1
- package/src/Rds/Rds_Proxy.res +1 -2
- package/src/S3/S3_BucketV2.res +1 -3
- package/src/Scheduler/Scheduler.res +4 -0
- package/src/Scheduler/Scheduler.res.mjs +9 -0
- package/src/Scheduler/Scheduler_ScheduleGroup.res +14 -0
- package/src/Scheduler/Scheduler_ScheduleGroup.res.mjs +2 -0
- package/src/example/AlarmMailExample.res +34 -0
- package/src/example/AlarmMailExample.res.mjs +44 -0
- package/src/example/CustomDomainExample.res +2 -3
- package/src/example/MergedApiExample.res +1 -3
- package/tests/AppSync_Resolver_FunctionsTest.mjs +421 -28
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
exported `request` and `response` functions, ready to be passed as the `code`
|
|
5
5
|
field of AppSync_Resolver.makeUnitJsResolver / makePipelineJsResolver.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
7
|
+
let // ---------------------------------------------------------------------------
|
|
9
8
|
// Shared response snippets (inlined into each resolver's template literal)
|
|
10
9
|
// ---------------------------------------------------------------------------
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
resultResponseCode = `
|
|
13
12
|
export function response(ctx) {
|
|
14
13
|
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
15
14
|
return ctx.result;
|
|
@@ -30,19 +29,11 @@ export function response(ctx) {
|
|
|
30
29
|
let importUtil = `import { util } from '@aws-appsync/utils';`
|
|
31
30
|
|
|
32
31
|
/**
|
|
33
|
-
The caller-is-exempt test, emitted into a resolver's response.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
provider question is answered before the identity one.
|
|
39
|
-
|
|
40
|
-
In the RESPONSE rather than the request, because a `GetItem` has no
|
|
41
|
-
FilterExpression to carry a predicate: the row is fetched by key and the
|
|
42
|
-
decision is made on what came back. A `Query` could filter server-side, and
|
|
43
|
-
deliberately does not — a single-row read that filtered in one place and
|
|
44
|
-
guarded in another would have two implementations of one rule, and the cheaper
|
|
45
|
-
one is the one nobody would remember to change.
|
|
32
|
+
The caller-is-exempt test, emitted into a resolver's response. Mirrors
|
|
33
|
+
`Reventless.OwnerScope.resolve`, branch order included — an IAM-signed caller has
|
|
34
|
+
no `sub` because it is inside the trust boundary, not because it is anonymous.
|
|
35
|
+
In the response because `GetItem` has no FilterExpression; `Query` follows it so
|
|
36
|
+
one rule keeps one implementation.
|
|
46
37
|
*/
|
|
47
38
|
let ownerGuardPreamble = (~ownerField: string, ~elevatedGroups: array<string>) => {
|
|
48
39
|
let elevatedLiteral = elevatedGroups->Array.map(g => `'${g}'`)->Array.join(", ")
|
|
@@ -56,12 +47,9 @@ let ownerGuardPreamble = (~ownerField: string, ~elevatedGroups: array<string>) =
|
|
|
56
47
|
}
|
|
57
48
|
|
|
58
49
|
/**
|
|
59
|
-
The exemption test
|
|
60
|
-
`@owner`
|
|
61
|
-
|
|
62
|
-
Identical to the three lines `ownerGuardPreamble` opens with, and emitted only
|
|
63
|
-
when that preamble is absent — two `const _exempt` in one function body is a
|
|
64
|
-
syntax error, and two *different* definitions of exempt would be worse than one.
|
|
50
|
+
The exemption test alone, for a door that narrows retirement but declares no
|
|
51
|
+
`@owner`. Emitted only when `ownerGuardPreamble` is absent — two `const _exempt`
|
|
52
|
+
in one body is a syntax error, and two different ones would be worse.
|
|
65
53
|
*/
|
|
66
54
|
let exemptPreamble = (~elevatedGroups: array<string>) => {
|
|
67
55
|
let elevatedLiteral = elevatedGroups->Array.map(g => `'${g}'`)->Array.join(", ")
|
|
@@ -75,22 +63,14 @@ let exemptPreamble = (~elevatedGroups: array<string>) => {
|
|
|
75
63
|
|
|
76
64
|
/**
|
|
77
65
|
A by-key read's retirement guard: `_live(row)`, true when the caller may see it.
|
|
66
|
+
The post-read half of what `listAllItemsConnection` pushes into a
|
|
67
|
+
FilterExpression — `GetItem` has none to push into.
|
|
78
68
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
the owner guard beside it already makes its own.
|
|
69
|
+
`row[field] == null` keeps a row written before the annotation existed. Exemption
|
|
70
|
+
alone withholds a retired row until `includeRetired` is passed, so an operator's
|
|
71
|
+
ordinary read is as narrow as anyone's.
|
|
83
72
|
|
|
84
|
-
`
|
|
85
|
-
the `attribute_not_exists` half of the list's clause and every other adapter's
|
|
86
|
-
reading of an absent value.
|
|
87
|
-
|
|
88
|
-
Asking is required, not merely permitted: `_exempt` alone leaves a retired row
|
|
89
|
-
withheld until `includeRetired` is passed, so an operator's ordinary read is as
|
|
90
|
-
narrow as anyone's. An archive that is always underfoot is not an archive.
|
|
91
|
-
|
|
92
|
-
`~ownerScoped` says whether an `ownerGuardPreamble` has already been emitted into
|
|
93
|
-
the same body; when it has, this reuses its `_exempt` instead of redeclaring one.
|
|
73
|
+
`~ownerScoped` says an `ownerGuardPreamble` already declared `_exempt` here.
|
|
94
74
|
*/
|
|
95
75
|
let retiredGuardPreamble = (
|
|
96
76
|
~retiredField: option<string>,
|
|
@@ -116,23 +96,15 @@ let retiredGuardPreamble = (
|
|
|
116
96
|
}
|
|
117
97
|
|
|
118
98
|
/**
|
|
119
|
-
The `@owner` predicate for an index door, as a FilterExpression clause.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
set, the door already runs `authorizeIndexedAccess`: the caller must be in the
|
|
129
|
-
named group AND be the holder the auth table records for that index value. Those
|
|
130
|
-
rows are, by construction, other people's — an order assigned to a fulfilment
|
|
131
|
-
operator is owned by the customer who placed it — so ANDing `@owner` on top would
|
|
132
|
-
return nothing and revoke exactly the access the auth table was written to grant.
|
|
133
|
-
An explicit per-index rule is the deployment's answer for that door; this is the
|
|
134
|
-
default for doors that have none. `QueryDbResolvers_AppSync` decides which is
|
|
135
|
-
which and passes `ownerField` only for the latter.
|
|
99
|
+
The `@owner` predicate for an index door, as a FilterExpression clause. Same rule
|
|
100
|
+
and branch order as `listAllItemsConnection`'s, and pushed into the read for the
|
|
101
|
+
same reason.
|
|
102
|
+
|
|
103
|
+
**Not applied to a group-restricted index.** There `authorizeIndexedAccess`
|
|
104
|
+
already gates the caller, and the rows are by construction other people's — an
|
|
105
|
+
order assigned to a fulfilment operator is owned by the customer who placed it —
|
|
106
|
+
so ANDing `@owner` on top would revoke exactly what the auth table granted.
|
|
107
|
+
`QueryDbResolvers_AppSync` passes `ownerField` only for doors with no such rule.
|
|
136
108
|
*/
|
|
137
109
|
let ownerFilterClause = (~ownerField: option<string>, ~elevatedGroups: array<string>) =>
|
|
138
110
|
switch ownerField {
|
|
@@ -158,16 +130,12 @@ let ownerFilterClause = (~ownerField: option<string>, ~elevatedGroups: array<str
|
|
|
158
130
|
|
|
159
131
|
/**
|
|
160
132
|
The same retirement predicate as `retiredGuardPreamble`, for a door that reads
|
|
161
|
-
with `Query` and
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
`listAllItemsConnection`'s reasoning: an index door takes a `limit`, and
|
|
165
|
-
narrowing after the read would hand back a page of fewer rows than the caller
|
|
166
|
-
asked for while reporting nothing about why.
|
|
133
|
+
with `Query` and so has a FilterExpression to put it in. Pushed into the read
|
|
134
|
+
rather than applied after it, or the page comes back short with nothing said
|
|
135
|
+
about why.
|
|
167
136
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
instead of replacing them.
|
|
137
|
+
Appends to the `expression` / `names` / `values` the index templates already
|
|
138
|
+
build, so it composes with a caller's own filter arguments.
|
|
171
139
|
*/
|
|
172
140
|
let retiredFilterClause = (
|
|
173
141
|
~retiredField: option<string>,
|
|
@@ -207,13 +175,9 @@ ${assignments}
|
|
|
207
175
|
/**
|
|
208
176
|
A by-key read's response, refusing a row the caller does not own.
|
|
209
177
|
|
|
210
|
-
**Null, not an error
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
already answers `null` here, and a rule enforced differently per transport is
|
|
214
|
-
the failure mode owner scoping exists to avoid. And an error would confirm the
|
|
215
|
-
row exists to a caller who may not read it, which is a worse leak than the
|
|
216
|
-
ambiguity it removes.
|
|
178
|
+
**Null, not an error.** The in-process platform answers `null` here, and a rule
|
|
179
|
+
enforced differently per transport is what owner scoping exists to avoid. An
|
|
180
|
+
error would also confirm the row exists to a caller who may not read it.
|
|
217
181
|
*/
|
|
218
182
|
let ownerScopedResultResponse = (
|
|
219
183
|
~ownerField: option<string>,
|
|
@@ -226,15 +190,18 @@ let ownerScopedResultResponse = (
|
|
|
226
190
|
| _ =>
|
|
227
191
|
let ownerPart = switch ownerField {
|
|
228
192
|
// Takes the row it ignores: APPSYNC_JS type-checks the resolver, so a
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
193
|
+
// zero-parameter stub called as `_owns(row)` is TS2554 ("Expected 0
|
|
194
|
+
// arguments, but got 1") and AppSync rejects the whole resolver at create
|
|
195
|
+
// time with "The code contains one or more errors". Only a door that emits
|
|
196
|
+
// the call conditionally is safe with a bare `() => true`, and that is not a
|
|
197
|
+
// property worth relying on across three templates.
|
|
198
|
+
| None => "\n const _owns = (row) => true;"
|
|
235
199
|
| Some(field) =>
|
|
236
200
|
`
|
|
237
|
-
// ── owner scoping (generated) ──${ownerGuardPreamble(
|
|
201
|
+
// ── owner scoping (generated) ──${ownerGuardPreamble(
|
|
202
|
+
~ownerField=field,
|
|
203
|
+
~elevatedGroups,
|
|
204
|
+
)}`
|
|
238
205
|
}
|
|
239
206
|
let retiredPart = retiredGuardPreamble(
|
|
240
207
|
~retiredField,
|
|
@@ -245,7 +212,9 @@ let ownerScopedResultResponse = (
|
|
|
245
212
|
`
|
|
246
213
|
export function response(ctx) {
|
|
247
214
|
if (ctx.error) util.error(ctx.error.message, ctx.error.type);${ownerPart}${retiredPart}
|
|
248
|
-
return _owns(ctx.result)${retiredField->Option.isSome
|
|
215
|
+
return _owns(ctx.result)${retiredField->Option.isSome
|
|
216
|
+
? " && _live(ctx.result)"
|
|
217
|
+
: ""} ? ctx.result : null;
|
|
249
218
|
}`
|
|
250
219
|
}
|
|
251
220
|
|
|
@@ -261,15 +230,18 @@ let ownerScopedFirstResultResponse = (
|
|
|
261
230
|
| _ =>
|
|
262
231
|
let ownerPart = switch ownerField {
|
|
263
232
|
// Takes the row it ignores: APPSYNC_JS type-checks the resolver, so a
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
233
|
+
// zero-parameter stub called as `_owns(row)` is TS2554 ("Expected 0
|
|
234
|
+
// arguments, but got 1") and AppSync rejects the whole resolver at create
|
|
235
|
+
// time with "The code contains one or more errors". Only a door that emits
|
|
236
|
+
// the call conditionally is safe with a bare `() => true`, and that is not a
|
|
237
|
+
// property worth relying on across three templates.
|
|
238
|
+
| None => "\n const _owns = (row) => true;"
|
|
270
239
|
| Some(field) =>
|
|
271
240
|
`
|
|
272
|
-
// ── owner scoping (generated) ──${ownerGuardPreamble(
|
|
241
|
+
// ── owner scoping (generated) ──${ownerGuardPreamble(
|
|
242
|
+
~ownerField=field,
|
|
243
|
+
~elevatedGroups,
|
|
244
|
+
)}`
|
|
273
245
|
}
|
|
274
246
|
let retiredPart = retiredGuardPreamble(
|
|
275
247
|
~retiredField,
|
|
@@ -290,8 +262,7 @@ export function response(ctx) {
|
|
|
290
262
|
// ---------------------------------------------------------------------------
|
|
291
263
|
|
|
292
264
|
/** Pipeline resolver code with no pre-processing and a standard error-check response. */
|
|
293
|
-
let pipelinePassThrough =
|
|
294
|
-
`${importUtil}
|
|
265
|
+
let pipelinePassThrough = `${importUtil}
|
|
295
266
|
export function request(ctx) { return {}; }
|
|
296
267
|
${resultResponseCode}
|
|
297
268
|
`->Pulumi.Input.make
|
|
@@ -303,12 +274,9 @@ ${resultResponseCode}
|
|
|
303
274
|
/** Pipeline function (NONE datasource): decodes global ID and stashes typeName + localId.
|
|
304
275
|
|
|
305
276
|
Written without a `try`: APPSYNC_JS rejects try statements outright
|
|
306
|
-
(`@aws-appsync/no-try`),
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
run. An id that decodes to no `type:localId` pair therefore fails the one way
|
|
310
|
-
that is left: both halves stash as null, which is also what the guarded
|
|
311
|
-
version reported for a decode it could not parse. */
|
|
277
|
+
(`@aws-appsync/no-try`), and nothing is lost — `util.base64Decode` returns
|
|
278
|
+
bytes rather than throwing on malformed input, so an unparseable id stashes
|
|
279
|
+
both halves as null, which is what a guarded version reported anyway. */
|
|
312
280
|
let nodeDecodeGlobalId =
|
|
313
281
|
`${importUtil}
|
|
314
282
|
export function request(ctx) {
|
|
@@ -367,8 +335,7 @@ export function request(ctx) {
|
|
|
367
335
|
${ownerScopedResultResponse(~ownerField, ~elevatedGroups, ~retiredField?, ~retiredValues?)}
|
|
368
336
|
`->Pulumi.Input.make
|
|
369
337
|
|
|
370
|
-
let queryById =
|
|
371
|
-
`${importUtil}
|
|
338
|
+
let queryById = `${importUtil}
|
|
372
339
|
export function request(ctx) {
|
|
373
340
|
return {
|
|
374
341
|
operation: 'Query',
|
|
@@ -386,11 +353,11 @@ ${resultResponseCode}
|
|
|
386
353
|
Relay pagination: `first`/`after` (forward) or `last`/`before` (backward).
|
|
387
354
|
Cursor is base64 of the sort key value.
|
|
388
355
|
Returns a Relay `{ edges, pageInfo }` shape reusing the entity's `Connection` type. */
|
|
389
|
-
// A list in everything but its name, so it scopes the way `listAllItemsConnection`
|
|
356
|
+
let // A list in everything but its name, so it scopes the way `listAllItemsConnection`
|
|
390
357
|
// does — a FilterExpression on the request, not a guard on the response. The
|
|
391
358
|
// response is where the page is cut, and narrowing after that cut would report
|
|
392
359
|
// `hasNextPage` from a count the caller was never allowed to see.
|
|
393
|
-
|
|
360
|
+
queryItemsWithSortConditions = (
|
|
394
361
|
sortField: string,
|
|
395
362
|
~ownerField: option<string>=?,
|
|
396
363
|
~elevatedGroups: array<string>=[],
|
|
@@ -585,61 +552,196 @@ export function request(ctx) {
|
|
|
585
552
|
${resultResponseCode}
|
|
586
553
|
`->Pulumi.Input.make
|
|
587
554
|
|
|
555
|
+
// ---------------------------------------------------------------------------
|
|
556
|
+
// Paging a filtered read
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
|
|
588
559
|
/**
|
|
589
|
-
|
|
560
|
+
Rows a read may EXAMINE per page.
|
|
590
561
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
562
|
+
`Limit` applies before the FilterExpression, so reading `first` rows and returning
|
|
563
|
+
the survivors serves short — usually empty — pages under a selective filter. With
|
|
564
|
+
no loops in APPSYNC_JS the door reads wider instead and addresses the surplus by
|
|
565
|
+
position. 1 MB caps a page anyway, hence 1000; `filtered` is the JS expression
|
|
566
|
+
saying whether a filter was pushed down.
|
|
596
567
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
568
|
+
An unfiltered read takes one row past the page. DynamoDB hands back a token
|
|
569
|
+
whenever it stops AT the limit, end of data or not, so a read of exactly the page
|
|
570
|
+
reports a next page that comes back blank; the extra row is what says one exists.
|
|
600
571
|
*/
|
|
601
|
-
let
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
572
|
+
let pageWindowBudget = (~filtered: string) =>
|
|
573
|
+
`(${filtered} ? (_first > 1000 ? _first : 1000) : _first + _from + 1)`
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
The same budget for the full-list door, which may also read backward.
|
|
577
|
+
|
|
578
|
+
A backward page is the slice `[_from, _upTo)` of the window, so the read has to
|
|
579
|
+
reach `_upTo` rows into it — `_first + _from` would stop short and hand back a
|
|
580
|
+
page with its tail missing. Forward reads one row past the page, for the reason
|
|
581
|
+
`pageWindowBudget` gives; a backward page needs none, as one provably follows it.
|
|
582
|
+
*/
|
|
583
|
+
let listPageWindowBudget = {
|
|
584
|
+
let need = "(_backward ? _upTo : _first + _from + 1)"
|
|
585
|
+
`(parts.length > 0 ? (${need} > 1000 ? ${need} : 1000) : ${need})`
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
Decodes `after` into the window it names (`t`, the token opening it) and the row's
|
|
590
|
+
index among that window's matches (`n`). Pre-window cursors carried
|
|
591
|
+
`{token, index}` naming the window that follows — position -1 of it.
|
|
592
|
+
|
|
593
|
+
`p` is the one-character tag naming which read the window belongs to (`s` Scan,
|
|
594
|
+
`q` the owner-index Query). Absent reads as `s`: every cursor minted before the
|
|
595
|
+
tag existed came off a Scan. Only a door with more than one read tests it — see
|
|
596
|
+
`cursorPathGuard`.
|
|
597
|
+
*/
|
|
598
|
+
let cursorDecode = (~args: string) =>
|
|
599
|
+
`
|
|
600
|
+
let _window = null;
|
|
601
|
+
let _from = 0;
|
|
602
|
+
let _cursorPath = null;
|
|
603
|
+
if (${args}.after != null && ${args}.after !== '') {
|
|
604
|
+
const _c = JSON.parse(util.base64Decode(${args}.after));
|
|
605
|
+
_window = (_c.t !== undefined ? _c.t : _c.token) ?? null;
|
|
606
|
+
_from = _c.n !== undefined ? _c.n + 1 : 0;
|
|
607
|
+
_cursorPath = _c.p ?? 's';
|
|
608
|
+
}`
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
Refuses a cursor minted on this door's other read. The two branches are selectable
|
|
612
|
+
by the SAME caller across requests — an active-role switch mid-pagination flips
|
|
613
|
+
`_exempt` — and a `nextToken` continues the operation that issued it, so replaying
|
|
614
|
+
one on the other branch answers a different question without saying so.
|
|
615
|
+
|
|
616
|
+
Expects `_cursorPath` from `cursorDecode` and `_exempt` from the owner preamble.
|
|
617
|
+
*/
|
|
618
|
+
let cursorPathGuard = `
|
|
619
|
+
if (_cursorPath !== null && _cursorPath !== (_exempt ? 's' : 'q')) {
|
|
620
|
+
util.error('This cursor belongs to a different read of this list; restart from the first page.', 'CursorPathMismatch');
|
|
621
|
+
}`
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
The full-list door's cursor decode, which unlike `cursorDecode` may run backward.
|
|
625
|
+
|
|
626
|
+
A `{t, n}` cursor names a position INSIDE a read window, and a window is re-read
|
|
627
|
+
from its own token — which forward paging already relies on to resume mid-window.
|
|
628
|
+
Backward is the same move in the other direction: re-read the window `before`
|
|
629
|
+
names and cut the page that ends at `n`, `[n - first, n)`. No new cursor shape, no
|
|
630
|
+
index, and no assumption forward paging does not already make.
|
|
631
|
+
|
|
632
|
+
What it cannot do is cross into an EARLIER window: DynamoDB's continuation chain
|
|
633
|
+
walks one way, so a window cannot name the one before it. `_upTo <= 0` with a
|
|
634
|
+
non-null window is that case, and it is the only one still refused.
|
|
635
|
+
|
|
636
|
+
Declares `_first` (the response's slicing needs it before `connectionPageResponse`
|
|
637
|
+
would have declared it, so that one omits it here).
|
|
638
|
+
*/
|
|
639
|
+
let listCursorPreamble = `
|
|
640
|
+
const _first = ctx.args.first ?? 50;
|
|
641
|
+
let _window = null;
|
|
642
|
+
let _from = 0;
|
|
643
|
+
let _cursorPath = null;
|
|
644
|
+
let _backward = false;
|
|
645
|
+
let _upTo = -1;
|
|
646
|
+
if (ctx.args.before != null && ctx.args.before !== '') {
|
|
647
|
+
const _c = JSON.parse(util.base64Decode(ctx.args.before));
|
|
648
|
+
_window = (_c.t !== undefined ? _c.t : _c.token) ?? null;
|
|
649
|
+
_cursorPath = _c.p ?? 's';
|
|
650
|
+
_backward = true;
|
|
651
|
+
_upTo = _c.n !== undefined ? _c.n : 0;
|
|
652
|
+
_from = (_upTo - _first) > 0 ? (_upTo - _first) : 0;
|
|
653
|
+
} else if (ctx.args.after != null && ctx.args.after !== '') {
|
|
654
|
+
const _c = JSON.parse(util.base64Decode(ctx.args.after));
|
|
655
|
+
_window = (_c.t !== undefined ? _c.t : _c.token) ?? null;
|
|
656
|
+
_from = _c.n !== undefined ? _c.n + 1 : 0;
|
|
657
|
+
_cursorPath = _c.p ?? 's';
|
|
658
|
+
}`
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
Cuts the requested page out of the returned window. Expects `items` (sorted, if the
|
|
662
|
+
door sorts) plus `_window` / `_from` from `cursorDecode`. `pathExpr` is the JS
|
|
663
|
+
expression naming which read minted these cursors, for a door that has two.
|
|
664
|
+
|
|
665
|
+
`bidirectional` is the full-list door, whose page may have been cut backward: it
|
|
666
|
+
reads `_backward` / `_upTo` from `listCursorPreamble` and takes `_first` from
|
|
667
|
+
there rather than declaring its own.
|
|
668
|
+
*/
|
|
669
|
+
let connectionPageResponse = (~pathExpr: option<string>=?, ~bidirectional: bool=false) => {
|
|
670
|
+
let tag = switch pathExpr {
|
|
671
|
+
| None => ""
|
|
672
|
+
| Some(e) => `, p: ${e}`
|
|
673
|
+
}
|
|
674
|
+
let firstDecl = bidirectional ? "" : "\n const _first = ctx.args.first ?? 50;"
|
|
675
|
+
let slice = bidirectional
|
|
676
|
+
? `
|
|
677
|
+
const _rest = _backward ? items.slice(_from, _upTo) : items.slice(_from);
|
|
678
|
+
const _page = _backward ? _rest : _rest.slice(0, _first);
|
|
679
|
+
// A backward page was cut from ahead, so a next page provably exists — the one
|
|
680
|
+
// the caller came from — whatever this window's tail looks like.
|
|
681
|
+
const _more = _backward ? true : _rest.length > _first;`
|
|
682
|
+
: `
|
|
683
|
+
const _rest = items.slice(_from);
|
|
684
|
+
const _page = _rest.slice(0, _first);
|
|
685
|
+
const _more = _rest.length > _first;`
|
|
686
|
+
// Only what the door can actually serve. `!!ctx.args.after` said "a page
|
|
687
|
+
// precedes this one", which is a different claim: at a window boundary one does
|
|
688
|
+
// and this door cannot reach it, so the client drew a Prev that always errored.
|
|
689
|
+
let hasPrevious = bidirectional ? "_from > 0" : "!!ctx.args.after"
|
|
690
|
+
`${firstDecl}${slice}
|
|
691
|
+
const _next = ctx.result?.nextToken ?? null;
|
|
692
|
+
const _lastIndex = _page.length - 1;
|
|
693
|
+
// A row's cursor names its own position. The last row of a page that closes its
|
|
694
|
+
// window is the exception — no position follows it there, so it names the next
|
|
695
|
+
// window, or resuming from it answers blank.
|
|
696
|
+
const edges = _page.map((item, i) => ({
|
|
607
697
|
node: item,
|
|
608
|
-
cursor: util.base64Encode(JSON.stringify(
|
|
698
|
+
cursor: util.base64Encode(JSON.stringify(
|
|
699
|
+
(!_more && _next && i === _lastIndex)
|
|
700
|
+
? { t: _next, n: -1${tag} }
|
|
701
|
+
: { t: _window, n: _from + i${tag} }
|
|
702
|
+
)),
|
|
609
703
|
}));
|
|
610
|
-
|
|
704
|
+
// A window the filter emptied leaves no row to cut a cursor from; the token is
|
|
705
|
+
// the window's, so a client can step past it rather than restart.
|
|
706
|
+
const _boundary = _next ? util.base64Encode(JSON.stringify({ t: _next, n: -1${tag} })) : null;
|
|
611
707
|
return {
|
|
612
708
|
edges,
|
|
613
709
|
pageInfo: {
|
|
614
|
-
hasNextPage: !!
|
|
615
|
-
hasPreviousPage:
|
|
616
|
-
startCursor: edges.length > 0 ? edges[0].cursor :
|
|
617
|
-
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor :
|
|
710
|
+
hasNextPage: _more || !!_next,
|
|
711
|
+
hasPreviousPage: ${hasPrevious},
|
|
712
|
+
startCursor: edges.length > 0 ? edges[0].cursor : _boundary,
|
|
713
|
+
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : _boundary,
|
|
618
714
|
},
|
|
619
|
-
}
|
|
715
|
+
};`
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
The by-index door's response. Pages out of a read window exactly as
|
|
720
|
+
`listAllItemsConnection` does, and satisfies the `Connection!` the field has always
|
|
721
|
+
declared — returning `ctx.result` raw did not.
|
|
722
|
+
*/
|
|
723
|
+
let indexConnectionResponseCode = `
|
|
724
|
+
export function response(ctx) {
|
|
725
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
726
|
+
const items = ctx.result?.items ?? [];${cursorDecode(~args="ctx.args")}${connectionPageResponse()}
|
|
620
727
|
}`
|
|
621
728
|
|
|
622
729
|
/** Refuses backward paging, for the reason `listAllItemsConnection` gives: the
|
|
623
730
|
cursor is DynamoDB's own continuation token, which only walks forward, so
|
|
624
|
-
`last`/`before` cannot be
|
|
625
|
-
a different question without saying so.
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
make every client feature-detect — and the local backend refuses them with the
|
|
629
|
-
same message, so the door reads the same either side of a deploy. */
|
|
731
|
+
`last`/`before` cannot be honored, and handing back the forward page would answer
|
|
732
|
+
a different question without saying so. The arguments stay declared — one that
|
|
733
|
+
came and went with the index's shape would make every client feature-detect — and
|
|
734
|
+
the local backend refuses them with the same message. */
|
|
630
735
|
let indexBackwardPagingGuard = `
|
|
631
736
|
if (args.before != null || args.last != null) {
|
|
632
737
|
util.error('Backward pagination (last/before) is not supported on by-index connections; use first/after.', 'UnsupportedPagination');
|
|
633
738
|
}`
|
|
634
739
|
|
|
635
|
-
/** Decodes the Relay `after` cursor back to the
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const parsed = JSON.parse(util.base64Decode(args.after));
|
|
641
|
-
after = parsed.token ?? null;
|
|
642
|
-
}`
|
|
740
|
+
/** Decodes the Relay `after` cursor back to the read window the response side
|
|
741
|
+
encoded, and sizes the window this read may examine. Mirrors
|
|
742
|
+
`listAllItemsConnection`'s request half. */
|
|
743
|
+
let indexCursorPreamble = `${cursorDecode(~args="args")}
|
|
744
|
+
const _first = args.first ?? 50;`
|
|
643
745
|
|
|
644
746
|
// The arguments the by-index door declares, none of which is a column to match
|
|
645
747
|
// on. `includeRetired` is a request to lift a restriction and the rest are
|
|
@@ -688,13 +790,17 @@ export function request(ctx) {
|
|
|
688
790
|
values[':' + key] = '' + value;
|
|
689
791
|
}
|
|
690
792
|
});
|
|
691
|
-
${ownerFilterClause(~ownerField, ~elevatedGroups)}${retiredFilterClause(
|
|
793
|
+
${ownerFilterClause(~ownerField, ~elevatedGroups)}${retiredFilterClause(
|
|
794
|
+
~retiredField,
|
|
795
|
+
~retiredValues,
|
|
796
|
+
~elevatedGroups,
|
|
797
|
+
)}${indexCursorPreamble}
|
|
692
798
|
const result = {
|
|
693
799
|
operation: 'Query',
|
|
694
800
|
query,
|
|
695
801
|
index: '${index}',
|
|
696
|
-
limit: (
|
|
697
|
-
nextToken:
|
|
802
|
+
limit: ${pageWindowBudget(~filtered="expression")},
|
|
803
|
+
nextToken: _window,
|
|
698
804
|
scanIndexForward: (args.forward ?? true)
|
|
699
805
|
};
|
|
700
806
|
if (expression) {
|
|
@@ -751,13 +857,17 @@ export function request(ctx) {
|
|
|
751
857
|
values[':' + key] = '' + value;
|
|
752
858
|
}
|
|
753
859
|
});
|
|
754
|
-
${ownerFilterClause(~ownerField, ~elevatedGroups)}${retiredFilterClause(
|
|
860
|
+
${ownerFilterClause(~ownerField, ~elevatedGroups)}${retiredFilterClause(
|
|
861
|
+
~retiredField,
|
|
862
|
+
~retiredValues,
|
|
863
|
+
~elevatedGroups,
|
|
864
|
+
)}${indexCursorPreamble}
|
|
755
865
|
const result = {
|
|
756
866
|
operation: 'Query',
|
|
757
867
|
query,
|
|
758
868
|
index: '${index}',
|
|
759
|
-
limit: (
|
|
760
|
-
nextToken:
|
|
869
|
+
limit: ${pageWindowBudget(~filtered="expression")},
|
|
870
|
+
nextToken: _window,
|
|
761
871
|
scanIndexForward: (args.forward ?? true)
|
|
762
872
|
};
|
|
763
873
|
if (expression) {
|
|
@@ -772,8 +882,7 @@ ${indexConnectionResponseCode}
|
|
|
772
882
|
// DynamoDB read — list all
|
|
773
883
|
// ---------------------------------------------------------------------------
|
|
774
884
|
|
|
775
|
-
let listAllItems =
|
|
776
|
-
`${importUtil}
|
|
885
|
+
let listAllItems = `${importUtil}
|
|
777
886
|
export function request(ctx) {
|
|
778
887
|
return {
|
|
779
888
|
operation: 'Scan',
|
|
@@ -789,48 +898,36 @@ ${resultResponseCode}
|
|
|
789
898
|
// ---------------------------------------------------------------------------
|
|
790
899
|
|
|
791
900
|
/**
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
* sort fields; `@scanSort` on a non-indexed field is per-page even then.
|
|
810
|
-
*
|
|
811
|
-
* Empty-string and null filter values are treated as "no filter" — consistent with the
|
|
812
|
-
* in-memory adapter so clients don't need to conditionally omit keys.
|
|
813
|
-
*/
|
|
901
|
+
Scan behind `filter: {search?, searchPrefix?, ids?, <field>Eq?, <field>From?,
|
|
902
|
+
<field>To?}` and `orderBy: {field, direction}`. `search` / `searchPrefix` become
|
|
903
|
+
`contains` / `begins_with` on `labelField` (case-sensitive — a case-insensitive
|
|
904
|
+
match wants a lowercased projected column); `ids` becomes `#id IN (…)`; the
|
|
905
|
+
per-field forms become `=` / `>=` / `<=`.
|
|
906
|
+
|
|
907
|
+
`orderBy` sorts in the JS runtime over the read window, not globally: Scan returns
|
|
908
|
+
items in indeterminate order and `ScanIndexForward` is Query-only. Empty and null
|
|
909
|
+
filter values mean "no filter", as they do in-memory.
|
|
910
|
+
|
|
911
|
+
With `ownerIndex` the door has **two** reads against the same data source, chosen
|
|
912
|
+
by whether the caller is exempt from owner scoping: a Query on the derived
|
|
913
|
+
`@owner` index for a scoped caller, the Scan above for everyone else. A user
|
|
914
|
+
`filter` still lands in a FilterExpression on top of the Query's key condition, so
|
|
915
|
+
a scoped caller searching their own rows can still get a short page — but bounded
|
|
916
|
+
by their row count rather than the table's.
|
|
917
|
+
*/
|
|
814
918
|
let listAllItemsConnection = (
|
|
815
919
|
~labelField: string,
|
|
816
920
|
~filterFields: array<string>=[],
|
|
817
921
|
~rangeFields: array<string>=[],
|
|
818
922
|
~sortFields: array<string>=[],
|
|
819
|
-
//
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
// table also holds `deploy-schema:*` / `plugin-info:*` rows with no `name`). Those
|
|
824
|
-
// rows would otherwise resolve `name`/`status`/`version` to null and violate the
|
|
825
|
-
// non-null GraphQL connection schema, nulling the whole connection.
|
|
923
|
+
// An always-on `attribute_exists(#<attr>)` clause, for a read model whose table
|
|
924
|
+
// co-hosts bookkeeping rows written outside the projection (the Plugins admin RM's
|
|
925
|
+
// `deploy-schema:*` / `plugin-info:*` rows carry no `name`). Those rows resolve
|
|
926
|
+
// non-null fields to null and take the whole Connection with them.
|
|
826
927
|
~requireAttribute: option<string>=?,
|
|
827
|
-
// The state's `@owner` field
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
// configuration value at request time, so the deploy is the only chance to
|
|
831
|
-
// state it. Changing the elevated-group list therefore requires a redeploy,
|
|
832
|
-
// which is worth knowing and is why it is a deployment-level setting rather
|
|
833
|
-
// than a per-request one.
|
|
928
|
+
// The state's `@owner` field and the groups exempt from scoping. Baked in
|
|
929
|
+
// because no Lambda sits in this path to read a value at request time, so the
|
|
930
|
+
// elevated-group list changes only on redeploy.
|
|
834
931
|
~ownerField: option<string>=?,
|
|
835
932
|
~elevatedGroups: array<string>=[],
|
|
836
933
|
// The state's `@retired` field, when it declares one. Baked in for the same
|
|
@@ -840,9 +937,17 @@ let listAllItemsConnection = (
|
|
|
840
937
|
// The states that retire the row, for the state form of the annotation.
|
|
841
938
|
// Absent is the boolean form, where the value is always `true`.
|
|
842
939
|
~retiredValues: option<array<string>>=?,
|
|
940
|
+
// The index `@owner` derives, and its sort key. Present turns the owner
|
|
941
|
+
// predicate from a post-read sieve into a key condition for a scoped caller.
|
|
942
|
+
~ownerIndex: option<string>=?,
|
|
943
|
+
~ownerIndexSortField: option<string>=?,
|
|
843
944
|
) => {
|
|
945
|
+
// The Query branch needs `ownerField` to key on; an index without one would
|
|
946
|
+
// have nothing to name in the key condition.
|
|
947
|
+
let ownerIndex = ownerField->Option.isSome ? ownerIndex : None
|
|
844
948
|
let requireAttributeClause = switch requireAttribute {
|
|
845
|
-
| Some(attr) =>
|
|
949
|
+
| Some(attr) =>
|
|
950
|
+
`
|
|
846
951
|
names['#${attr}'] = '${attr}';
|
|
847
952
|
parts.push('attribute_exists(#${attr})');`
|
|
848
953
|
| None => ""
|
|
@@ -851,9 +956,11 @@ let listAllItemsConnection = (
|
|
|
851
956
|
// it. The branch ORDER is the part that has to match: provider first, because
|
|
852
957
|
// an IAM-signed service caller has no `sub` for a reason that has nothing to do
|
|
853
958
|
// with being anonymous, and must not be refused as though it did.
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
959
|
+
//
|
|
960
|
+
// Emitted in BOTH halves of the resolver when an owner index is in play: the
|
|
961
|
+
// response has to know which read minted the cursors it hands out, and that is
|
|
962
|
+
// the same question.
|
|
963
|
+
let ownerIdentityPreamble = {
|
|
857
964
|
let elevatedLiteral = elevatedGroups->Array.map(g => `'${g}'`)->Array.join(", ")
|
|
858
965
|
`
|
|
859
966
|
// ── owner scoping (generated) ──
|
|
@@ -866,7 +973,16 @@ let listAllItemsConnection = (
|
|
|
866
973
|
const _elevated = [${elevatedLiteral}];
|
|
867
974
|
// No identity at all, or an identity with no \`sub\`, is the IAM service caller
|
|
868
975
|
// the API also accepts — inside the trust boundary, and exempt.
|
|
869
|
-
const _exempt = _sub == null || _groups.some(g => _elevated.indexOf(g) >= 0)
|
|
976
|
+
const _exempt = _sub == null || _groups.some(g => _elevated.indexOf(g) >= 0);`
|
|
977
|
+
}
|
|
978
|
+
let ownerClause = switch (ownerField, ownerIndex) {
|
|
979
|
+
| (None, _) => ""
|
|
980
|
+
// With an index the predicate is the Query's key condition, so nothing is
|
|
981
|
+
// pushed into the filter — and nothing may be: an expressionName the filter
|
|
982
|
+
// never references is a ValidationException, not a harmless extra.
|
|
983
|
+
| (Some(_), Some(_)) => ownerIdentityPreamble
|
|
984
|
+
| (Some(field), None) =>
|
|
985
|
+
`${ownerIdentityPreamble}
|
|
870
986
|
if (!_exempt) {
|
|
871
987
|
names['#owner'] = '${field}';
|
|
872
988
|
values[':owner'] = util.dynamodb.toDynamoDB(_sub);
|
|
@@ -874,32 +990,23 @@ let listAllItemsConnection = (
|
|
|
874
990
|
}`
|
|
875
991
|
}
|
|
876
992
|
// ── retirement narrowing (generated) ──
|
|
877
|
-
// Reuses `_exempt` when the owner clause
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
// `includeRetired` IS read from ctx.args, unlike the owner predicate, and the
|
|
882
|
-
// difference is deliberate: this argument does not say which rows the caller
|
|
883
|
-
// wants, it asks to lift a restriction, and it is honoured only inside the
|
|
884
|
-
// `_exempt` branch. A non-exempt caller passing it changes nothing.
|
|
993
|
+
// Reuses `_exempt` when the owner clause computed it; either clause may be the
|
|
994
|
+
// only one present. `includeRetired` IS read from ctx.args, unlike the owner
|
|
995
|
+
// predicate — it asks to lift a restriction rather than naming rows, and is
|
|
996
|
+
// honored only inside `_exempt`.
|
|
885
997
|
//
|
|
886
|
-
// `attribute_not_exists OR = false` rather than `<> true
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
//
|
|
891
|
-
// The state form compares `<>` against the retiring state instead, under the
|
|
892
|
-
// same `attribute_not_exists` guard and for the same reason. An equality
|
|
893
|
-
// predicate over an enum-valued attribute indexes exactly as a boolean one
|
|
894
|
-
// does, so the warning about an unindexed retirement field carries over
|
|
895
|
-
// unchanged.
|
|
998
|
+
// `attribute_not_exists OR = false` rather than `<> true`, because `<>` does not
|
|
999
|
+
// match a missing attribute and the view would empty out the day the annotation
|
|
1000
|
+
// lands. The state form compares `<>` against the retiring state under the same
|
|
1001
|
+
// guard.
|
|
896
1002
|
let retiredClause = switch retiredField {
|
|
897
1003
|
| None => ""
|
|
898
1004
|
| Some(field) =>
|
|
899
1005
|
let elevatedLiteral = elevatedGroups->Array.map(g => `'${g}'`)->Array.join(", ")
|
|
900
1006
|
let exemptPrelude = switch ownerField {
|
|
901
1007
|
| Some(_) => ""
|
|
902
|
-
| None =>
|
|
1008
|
+
| None =>
|
|
1009
|
+
`
|
|
903
1010
|
const _id = ctx.identity;
|
|
904
1011
|
const _sub = _id == null ? null : _id.sub;
|
|
905
1012
|
const _groups = (_id != null && _id.claims != null && _id.claims['cognito:groups']) || [];
|
|
@@ -920,10 +1027,13 @@ ${switch retiredValues {
|
|
|
920
1027
|
// once for the whole clause — a row that states no lifecycle is not
|
|
921
1028
|
// retired, the same reading every other adapter takes.
|
|
922
1029
|
| Some(states) =>
|
|
923
|
-
let placeholders =
|
|
1030
|
+
let placeholders =
|
|
1031
|
+
states->Array.mapWithIndex((state, i) => (`:retiredValue${Int.toString(i)}`, state))
|
|
924
1032
|
let assignments =
|
|
925
1033
|
placeholders
|
|
926
|
-
->Array.map(((ph, state)) =>
|
|
1034
|
+
->Array.map(((ph, state)) =>
|
|
1035
|
+
` values['${ph}'] = util.dynamodb.toDynamoDB('${state}');`
|
|
1036
|
+
)
|
|
927
1037
|
->Array.join("\n")
|
|
928
1038
|
let comparisons =
|
|
929
1039
|
placeholders->Array.map(((ph, _)) => `#retired <> ${ph}`)->Array.join(" AND ")
|
|
@@ -939,16 +1049,19 @@ ${switch retiredValues {
|
|
|
939
1049
|
}
|
|
940
1050
|
let filterClauses =
|
|
941
1051
|
filterFields
|
|
942
|
-
->Array.map(f =>
|
|
1052
|
+
->Array.map(f =>
|
|
1053
|
+
`
|
|
943
1054
|
if (filter.${f}Eq !== undefined && filter.${f}Eq !== null && filter.${f}Eq !== '') {
|
|
944
1055
|
names['#${f}'] = '${f}';
|
|
945
1056
|
values[':${f}Eq'] = util.dynamodb.toDynamoDB(filter.${f}Eq);
|
|
946
1057
|
parts.push('#${f} = :${f}Eq');
|
|
947
|
-
}`
|
|
1058
|
+
}`
|
|
1059
|
+
)
|
|
948
1060
|
->Array.join("")
|
|
949
1061
|
let rangeClauses =
|
|
950
1062
|
rangeFields
|
|
951
|
-
->Array.map(f =>
|
|
1063
|
+
->Array.map(f =>
|
|
1064
|
+
`
|
|
952
1065
|
if (filter.${f}From !== undefined && filter.${f}From !== null && filter.${f}From !== '') {
|
|
953
1066
|
names['#${f}'] = '${f}';
|
|
954
1067
|
values[':${f}From'] = util.dynamodb.toDynamoDB(filter.${f}From);
|
|
@@ -958,29 +1071,32 @@ ${switch retiredValues {
|
|
|
958
1071
|
names['#${f}'] = '${f}';
|
|
959
1072
|
values[':${f}To'] = util.dynamodb.toDynamoDB(filter.${f}To);
|
|
960
1073
|
parts.push('#${f} <= :${f}To');
|
|
961
|
-
}`
|
|
1074
|
+
}`
|
|
1075
|
+
)
|
|
962
1076
|
->Array.join("")
|
|
963
|
-
let sortFieldsLiteral =
|
|
964
|
-
|
|
1077
|
+
let sortFieldsLiteral = sortFields->Array.map(f => `'${f}'`)->Array.join(", ")
|
|
1078
|
+
// When the Query branch already ordered on the index's own sort key, the page
|
|
1079
|
+
// arrives globally ordered and re-sorting it here is the one way to break that
|
|
1080
|
+
// order — a sort over a page is not a sort over the caller's rows.
|
|
1081
|
+
let sortGuard = switch (ownerIndex, ownerIndexSortField) {
|
|
1082
|
+
| (Some(_), Some(_)) => "!_indexOrdered && "
|
|
1083
|
+
| _ => ""
|
|
1084
|
+
}
|
|
965
1085
|
let sortBlock = if sortFields->Array.length == 0 {
|
|
966
1086
|
""
|
|
967
1087
|
} else {
|
|
968
|
-
// APPSYNC_JS 1.0.0 forbids
|
|
969
|
-
//
|
|
970
|
-
//
|
|
971
|
-
//
|
|
972
|
-
//
|
|
973
|
-
// Numeric fields get zero-padded so lex order matches numeric order for
|
|
974
|
-
// non-negative values (typical for IDs, counts, timestamps). Negatives sort
|
|
975
|
-
// lexicographically — acceptable since DynamoDB sort keys are rarely signed
|
|
976
|
-
// numbers. Nulls split out and append to the end regardless of direction.
|
|
1088
|
+
// APPSYNC_JS 1.0.0 forbids comparator sorts, loops, recursion and ++/--, so
|
|
1089
|
+
// this is a schwartzian transform: encode each item as `<sortKey>\x01<json>`,
|
|
1090
|
+
// default-sort lexicographically, reverse for DESC, decode. Numbers are
|
|
1091
|
+
// zero-padded so lex order matches numeric order for non-negative values;
|
|
1092
|
+
// nulls split out and append to the end either way.
|
|
977
1093
|
`
|
|
978
1094
|
// Per-page sort (Scan returns items in indeterminate order; ScanIndexForward
|
|
979
1095
|
// does not apply to Scan). Global ordering across pages requires v1.5 index
|
|
980
1096
|
// promotion; @scanSort is per-page even then.
|
|
981
1097
|
const orderBy = ctx.args.orderBy;
|
|
982
1098
|
const sortFields = [${sortFieldsLiteral}];
|
|
983
|
-
if (orderBy && orderBy.field && sortFields.indexOf(orderBy.field) >= 0) {
|
|
1099
|
+
if (${sortGuard}orderBy && orderBy.field && sortFields.indexOf(orderBy.field) >= 0) {
|
|
984
1100
|
const field = orderBy.field;
|
|
985
1101
|
const nulls = items.filter(it => it[field] === null || it[field] === undefined);
|
|
986
1102
|
const nonNulls = items.filter(it => it[field] !== null && it[field] !== undefined);
|
|
@@ -996,13 +1112,69 @@ ${switch retiredValues {
|
|
|
996
1112
|
items = encoded.map(e => JSON.parse(e.split('\\x01')[1])).concat(nulls);
|
|
997
1113
|
}`
|
|
998
1114
|
}
|
|
1115
|
+
// Did the Query branch order this page itself? Answered the same way in both
|
|
1116
|
+
// halves, because both need it: the request to set `scanIndexForward`, the
|
|
1117
|
+
// response to leave an already-ordered page alone.
|
|
1118
|
+
let indexOrderedExpr = switch (ownerIndex, ownerIndexSortField) {
|
|
1119
|
+
| (Some(_), Some(sf)) => `!_exempt && !!(ctx.args.orderBy && ctx.args.orderBy.field === '${sf}')`
|
|
1120
|
+
| _ => "false"
|
|
1121
|
+
}
|
|
1122
|
+
// The two reads, chosen by the same test that used to choose a predicate.
|
|
1123
|
+
// Both target the one data source the resolver is attached to, so this is a
|
|
1124
|
+
// branch inside one resolver — no second field, no second data source, no
|
|
1125
|
+
// client change.
|
|
1126
|
+
let requestOperation = switch (ownerField, ownerIndex) {
|
|
1127
|
+
| (Some(field), Some(index)) =>
|
|
1128
|
+
`
|
|
1129
|
+
const _indexOrdered = ${indexOrderedExpr};
|
|
1130
|
+
const req = _exempt
|
|
1131
|
+
? {
|
|
1132
|
+
operation: 'Scan',
|
|
1133
|
+
limit: ${listPageWindowBudget},
|
|
1134
|
+
nextToken: _window,
|
|
1135
|
+
}
|
|
1136
|
+
: {
|
|
1137
|
+
operation: 'Query',
|
|
1138
|
+
index: '${index}',
|
|
1139
|
+
query: {
|
|
1140
|
+
expression: '#owner = :owner',
|
|
1141
|
+
expressionNames: { '#owner': '${field}' },
|
|
1142
|
+
expressionValues: { ':owner': util.dynamodb.toDynamoDB(_sub) },
|
|
1143
|
+
},
|
|
1144
|
+
limit: ${listPageWindowBudget},
|
|
1145
|
+
nextToken: _window,
|
|
1146
|
+
scanIndexForward: !(_indexOrdered && ctx.args.orderBy.direction === 'DESC'),
|
|
1147
|
+
};`
|
|
1148
|
+
| _ =>
|
|
1149
|
+
`
|
|
1150
|
+
const req = {
|
|
1151
|
+
operation: 'Scan',
|
|
1152
|
+
limit: ${listPageWindowBudget},
|
|
1153
|
+
nextToken: _window,
|
|
1154
|
+
};`
|
|
1155
|
+
}
|
|
1156
|
+
// Only a door with two reads tests the tag, and only that door stamps one.
|
|
1157
|
+
let requestPathGuard = ownerIndex->Option.isSome ? cursorPathGuard : ""
|
|
1158
|
+
let responsePathPreamble = switch ownerIndex {
|
|
1159
|
+
| None => ""
|
|
1160
|
+
| Some(_) =>
|
|
1161
|
+
`${ownerIdentityPreamble}
|
|
1162
|
+
const _path = _exempt ? 's' : 'q';
|
|
1163
|
+
const _indexOrdered = ${indexOrderedExpr};`
|
|
1164
|
+
}
|
|
1165
|
+
let pageResponse = switch ownerIndex {
|
|
1166
|
+
| None => connectionPageResponse(~bidirectional=true)
|
|
1167
|
+
| Some(_) => connectionPageResponse(~pathExpr="_path", ~bidirectional=true)
|
|
1168
|
+
}
|
|
999
1169
|
`${importUtil}
|
|
1000
1170
|
export function request(ctx) {
|
|
1001
|
-
//
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
1004
|
-
|
|
1005
|
-
|
|
1171
|
+
// 'before' IS served — the page is cut backward out of the window the cursor
|
|
1172
|
+
// names. 'last' is not: "the last N rows of the list" needs the end of the
|
|
1173
|
+
// list, which a forward-only Scan cursor cannot reach. The ordered
|
|
1174
|
+
// {single}Items connection (queryItemsWithSortConditions) has a real keyset
|
|
1175
|
+
// cursor and honours both — direct callers who need 'last' there.
|
|
1176
|
+
if (ctx.args.last != null) {
|
|
1177
|
+
util.error('last is not supported on full-list connections; page backward with first and before.', 'UnsupportedPagination');
|
|
1006
1178
|
}
|
|
1007
1179
|
const filter = ctx.args.filter ?? {};
|
|
1008
1180
|
const names = {};
|
|
@@ -1027,18 +1199,14 @@ export function request(ctx) {
|
|
|
1027
1199
|
});
|
|
1028
1200
|
parts.push('#id IN (' + placeholders.join(', ') + ')');
|
|
1029
1201
|
}${filterClauses}${rangeClauses}${requireAttributeClause}${ownerClause}${retiredClause}
|
|
1030
|
-
|
|
1031
|
-
//
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
operation: 'Scan',
|
|
1039
|
-
limit: (ctx.args.first ?? 50),
|
|
1040
|
-
nextToken: after,
|
|
1041
|
-
};
|
|
1202
|
+
${listCursorPreamble}${requestPathGuard}
|
|
1203
|
+
// The one page a cursor cannot reach: it begins in an earlier window, and a
|
|
1204
|
+
// continuation token cannot name the one before it. Refused by itself rather
|
|
1205
|
+
// than folded into the 'last' guard, because the two are different limits and a
|
|
1206
|
+
// caller can act on this one (page forward from the start).
|
|
1207
|
+
if (_backward && _upTo <= 0 && _window !== null) {
|
|
1208
|
+
util.error('The previous page begins in an earlier read window, which this cursor cannot name; page forward from the start.', 'UnsupportedPagination');
|
|
1209
|
+
}${requestOperation}
|
|
1042
1210
|
if (parts.length > 0) {
|
|
1043
1211
|
req.filter = {
|
|
1044
1212
|
expression: parts.join(' AND '),
|
|
@@ -1050,30 +1218,7 @@ export function request(ctx) {
|
|
|
1050
1218
|
}
|
|
1051
1219
|
export function response(ctx) {
|
|
1052
1220
|
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
1053
|
-
let items = ctx.result?.items ?? [];${sortBlock}
|
|
1054
|
-
// One Scan continuation token per page; encode it (with the item's page index for a
|
|
1055
|
-
// unique, opaque Relay cursor). The request side decodes .token back to the raw
|
|
1056
|
-
// DynamoDB nextToken (Fix 1).
|
|
1057
|
-
const next = ctx.result?.nextToken ?? null;
|
|
1058
|
-
const edges = items.map((item, i) => ({
|
|
1059
|
-
node: item,
|
|
1060
|
-
cursor: util.base64Encode(JSON.stringify({ token: next, index: i })),
|
|
1061
|
-
}));
|
|
1062
|
-
// A filtered/1MB-capped page can be empty or short while next is still set (limit
|
|
1063
|
-
// caps rows scanned, not returned). The token is page-level, so synthesise a
|
|
1064
|
-
// boundary cursor from it alone so a client can resume past a fully-filtered-out
|
|
1065
|
-
// page instead of restarting from page 1 (Fix 3). The request only reads .token,
|
|
1066
|
-
// so index -1 is inert on resume.
|
|
1067
|
-
const boundary = next ? util.base64Encode(JSON.stringify({ token: next, index: -1 })) : null;
|
|
1068
|
-
return {
|
|
1069
|
-
edges,
|
|
1070
|
-
pageInfo: {
|
|
1071
|
-
hasNextPage: !!next,
|
|
1072
|
-
hasPreviousPage: !!ctx.args.after,
|
|
1073
|
-
startCursor: edges.length > 0 ? edges[0].cursor : boundary,
|
|
1074
|
-
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : boundary,
|
|
1075
|
-
},
|
|
1076
|
-
};
|
|
1221
|
+
let items = ctx.result?.items ?? [];${responsePathPreamble}${sortBlock}${listCursorPreamble}${pageResponse}
|
|
1077
1222
|
}
|
|
1078
1223
|
`->Pulumi.Input.make
|
|
1079
1224
|
}
|
|
@@ -1085,14 +1230,10 @@ export function response(ctx) {
|
|
|
1085
1230
|
/**
|
|
1086
1231
|
The response of a cross-table field (`@resolves`) over a Query-shaped read.
|
|
1087
1232
|
|
|
1088
|
-
The narrowing is the TARGET's, not the declaring view's
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
A nested field takes no `includeRetired` argument, so `_wantsRetired` is never
|
|
1093
|
-
true and a retired row never travels through one. A reference that must keep
|
|
1094
|
-
reading as a name after the archive took it is what `{list}Refs` + `@namedWhenRetired`
|
|
1095
|
-
answers.
|
|
1233
|
+
The narrowing is the TARGET's, not the declaring view's — the row is the target's,
|
|
1234
|
+
so it answers with the same `_owns` / `_live` guards its by-key doors use. A
|
|
1235
|
+
nested field takes no `includeRetired`, so a retired row never travels through
|
|
1236
|
+
one; `{list}Refs` + `@namedWhenRetired` is that door.
|
|
1096
1237
|
*/
|
|
1097
1238
|
let resolvedFieldResponse = (
|
|
1098
1239
|
~multi: bool,
|
|
@@ -1111,7 +1252,10 @@ let resolvedFieldResponse = (
|
|
|
1111
1252
|
| None => "\n const _owns = (row) => true;"
|
|
1112
1253
|
| Some(field) =>
|
|
1113
1254
|
`
|
|
1114
|
-
// ── owner scoping (generated) ──${ownerGuardPreamble(
|
|
1255
|
+
// ── owner scoping (generated) ──${ownerGuardPreamble(
|
|
1256
|
+
~ownerField=field,
|
|
1257
|
+
~elevatedGroups,
|
|
1258
|
+
)}`
|
|
1115
1259
|
}
|
|
1116
1260
|
let retiredPart = retiredGuardPreamble(
|
|
1117
1261
|
~retiredField,
|
|
@@ -1303,14 +1447,10 @@ ${response}
|
|
|
1303
1447
|
|
|
1304
1448
|
/** `@resolvesMany` — the parent's id array batch-read from the target's table.
|
|
1305
1449
|
|
|
1306
|
-
|
|
1307
|
-
`Pulumi.Output.apply
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
Same shape as `batchGetItemsByIds`, and narrowed by the same guards — the
|
|
1311
|
-
target's, since the rows are the target's. Missing ids come back as nulls in
|
|
1312
|
-
the result array (BatchGetItem preserves index correspondence) and are
|
|
1313
|
-
dropped, so the field is shorter rather than null-holed. */
|
|
1450
|
+
A plain string, because BatchGetItem's `tables` map keys on the literal name,
|
|
1451
|
+
which the adapter interpolates via `Pulumi.Output.apply`. Same shape and
|
|
1452
|
+
guards as `batchGetItemsByIds`; missing ids come back null and are dropped, so
|
|
1453
|
+
the field is shorter rather than null-holed. */
|
|
1314
1454
|
let resolveIds = (
|
|
1315
1455
|
~idsField: string,
|
|
1316
1456
|
~sortField: option<string>,
|
|
@@ -1318,13 +1458,14 @@ let resolveIds = (
|
|
|
1318
1458
|
~retiredField: option<string>=?,
|
|
1319
1459
|
~retiredValues: option<array<string>>=?,
|
|
1320
1460
|
~elevatedGroups: array<string>=[],
|
|
1321
|
-
) =>
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1461
|
+
) =>
|
|
1462
|
+
(tableName: string) => {
|
|
1463
|
+
let keysCode = switch sortField {
|
|
1464
|
+
| Some(sf) =>
|
|
1465
|
+
`id => ({ id: util.dynamodb.toString(id.id), ${sf}: util.dynamodb.toString(id.${sf}) })`
|
|
1466
|
+
| None => `id => ({ id: util.dynamodb.toString(id) })`
|
|
1467
|
+
}
|
|
1468
|
+
`${importUtil}
|
|
1328
1469
|
import { runtime } from '@aws-appsync/utils';
|
|
1329
1470
|
export function request(ctx) {
|
|
1330
1471
|
const idList = ctx.source.${idsField} ?? [];
|
|
@@ -1341,36 +1482,34 @@ export function request(ctx) {
|
|
|
1341
1482
|
}
|
|
1342
1483
|
export function response(ctx) {
|
|
1343
1484
|
if (ctx.error) util.error(ctx.error.message, ctx.error.type);${switch ownerField {
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1485
|
+
| None => ""
|
|
1486
|
+
| Some(field) => ownerGuardPreamble(~ownerField=field, ~elevatedGroups)
|
|
1487
|
+
}}${retiredGuardPreamble(
|
|
1488
|
+
~retiredField,
|
|
1489
|
+
~retiredValues,
|
|
1490
|
+
~elevatedGroups,
|
|
1491
|
+
~ownerScoped=ownerField->Option.isSome,
|
|
1492
|
+
)}
|
|
1352
1493
|
return (ctx.result?.data?.['${tableName}'] ?? []).filter(item =>
|
|
1353
1494
|
item !== null${ownerField->Option.isSome ? " && _owns(item)" : ""}${retiredField->Option.isSome
|
|
1354
|
-
|
|
1355
|
-
|
|
1495
|
+
? " && _live(item)"
|
|
1496
|
+
: ""});
|
|
1356
1497
|
}
|
|
1357
1498
|
`
|
|
1358
|
-
}
|
|
1499
|
+
}
|
|
1359
1500
|
|
|
1360
|
-
/** Batched-by-ids —
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
time, since BatchGetItem's `tables` map keys on the literal table name.
|
|
1365
|
-
Single-key tables only — composite-key BatchGetItem needs both pk + sk per
|
|
1366
|
-
key entry, which this template doesn't construct. */
|
|
1501
|
+
/** Batched-by-ids — reads `ctx.args.ids: [String!]!` through one BatchGetItem.
|
|
1502
|
+
Missing ids drop out; empty input short-circuits without hitting DDB. The
|
|
1503
|
+
table name is interpolated at deploy time, since BatchGetItem's `tables` map
|
|
1504
|
+
keys on the literal. Single-key tables only. */
|
|
1367
1505
|
let batchGetItemsByIds = (
|
|
1368
1506
|
~ownerField: option<string>=?,
|
|
1369
1507
|
~retiredField: option<string>=?,
|
|
1370
1508
|
~retiredValues: option<array<string>>=?,
|
|
1371
1509
|
~elevatedGroups: array<string>=[],
|
|
1372
|
-
) =>
|
|
1373
|
-
|
|
1510
|
+
) =>
|
|
1511
|
+
(tableName: string) =>
|
|
1512
|
+
`${importUtil}
|
|
1374
1513
|
import { runtime } from '@aws-appsync/utils';
|
|
1375
1514
|
export function request(ctx) {
|
|
1376
1515
|
const ids = ctx.args.ids ?? [];
|
|
@@ -1387,79 +1526,79 @@ export function request(ctx) {
|
|
|
1387
1526
|
}
|
|
1388
1527
|
export function response(ctx) {
|
|
1389
1528
|
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
1390
|
-
// BatchGetItem returns null
|
|
1391
|
-
//
|
|
1392
|
-
//
|
|
1393
|
-
// missing id makes the entire field fail with "Cannot return null for
|
|
1394
|
-
// non-nullable type" and the caller sees data=null. Filter the nulls so
|
|
1395
|
-
// the field returns just the items that were found.
|
|
1529
|
+
// BatchGetItem returns null for keys that don't exist, preserving index
|
|
1530
|
+
// correspondence. The SDL declares \`[T!]!\`, so one missing id would null the
|
|
1531
|
+
// whole field — drop them and return what was found.
|
|
1396
1532
|
// The owner and retirement guards the list pushes into a FilterExpression,
|
|
1397
1533
|
// applied after the read because BatchGetItem has none to push into. A row the
|
|
1398
1534
|
// caller does not own is dropped rather than refused, for the reason the
|
|
1399
1535
|
// single-key door answers null: distinguishing "not yours" from "not there"
|
|
1400
1536
|
// would make this door an oracle for which ids exist.${switch ownerField {
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1537
|
+
| None => ""
|
|
1538
|
+
| Some(field) => ownerGuardPreamble(~ownerField=field, ~elevatedGroups)
|
|
1539
|
+
}}${retiredGuardPreamble(
|
|
1540
|
+
~retiredField,
|
|
1541
|
+
~retiredValues,
|
|
1542
|
+
~elevatedGroups,
|
|
1543
|
+
~ownerScoped=ownerField->Option.isSome,
|
|
1544
|
+
)}
|
|
1409
1545
|
return (ctx.result?.data?.['${tableName}'] ?? []).filter(item =>
|
|
1410
1546
|
item !== null${ownerField->Option.isSome ? " && _owns(item)" : ""}${retiredField->Option.isSome
|
|
1411
|
-
|
|
1412
|
-
|
|
1547
|
+
? " && _live(item)"
|
|
1548
|
+
: ""});
|
|
1413
1549
|
}
|
|
1414
1550
|
`
|
|
1415
1551
|
|
|
1416
1552
|
/** The reference door — `{list}Refs(ids)`: what a caller holding a pointer to a
|
|
1417
1553
|
row may learn about it, and nothing else.
|
|
1418
1554
|
|
|
1419
|
-
The same BatchGetItem as `batchGetItemsByIds`, projected
|
|
1420
|
-
`{id, label, retired, retiredState}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
`namedWhenRetired` is what a retired row turns on: false drops it, exactly as
|
|
1425
|
-
every other door does; true lets it through with the state that withdrew it.
|
|
1426
|
-
The owner rule is applied either way and is not what the annotation lifts. */
|
|
1555
|
+
The same BatchGetItem as `batchGetItemsByIds`, projected to
|
|
1556
|
+
`{id, label, retired, retiredState}` by the SDL type, so the response builds
|
|
1557
|
+
three fields rather than deciding what to withhold. `namedWhenRetired` decides
|
|
1558
|
+
a retired row: false drops it, true names it. The owner rule applies either
|
|
1559
|
+
way — that is not what the annotation lifts. */
|
|
1427
1560
|
let refsByIds = (
|
|
1428
1561
|
~labelField: string,
|
|
1429
1562
|
~retiredField: option<string>,
|
|
1430
1563
|
~retiredValues: option<array<string>>,
|
|
1431
1564
|
~namedWhenRetired: bool,
|
|
1565
|
+
// The picture a reference shows this row by, already read off the state schema
|
|
1566
|
+
// by `Reventless.RowImage` — this template only bakes in the expression it
|
|
1567
|
+
// hands over. `None` for a view that declares none, which emits a literal
|
|
1568
|
+
// `null`, exactly what the nullable field promises.
|
|
1569
|
+
~imageExpr: option<string>=?,
|
|
1432
1570
|
~ownerField: option<string>=?,
|
|
1433
1571
|
~elevatedGroups: array<string>=[],
|
|
1434
|
-
) =>
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1572
|
+
) =>
|
|
1573
|
+
(tableName: string) => {
|
|
1574
|
+
let ownerGuard = switch ownerField {
|
|
1575
|
+
// Takes the row it ignores: APPSYNC_JS type-checks the resolver, so a
|
|
1576
|
+
// zero-parameter stub called as `_owns(row)` is TS2554 ("Expected 0
|
|
1577
|
+
// arguments, but got 1") and AppSync rejects the whole resolver at create
|
|
1578
|
+
// time with "The code contains one or more errors". Only a door that emits
|
|
1579
|
+
// the call conditionally is safe with a bare `() => true`, and that is not a
|
|
1580
|
+
// property worth relying on across three templates.
|
|
1581
|
+
| None => "\n const _owns = (row) => true;"
|
|
1582
|
+
| Some(field) => ownerGuardPreamble(~ownerField=field, ~elevatedGroups)
|
|
1583
|
+
}
|
|
1584
|
+
// Retirement, in the vocabulary the row itself uses: a member test for the
|
|
1585
|
+
// state form, truthiness for the boolean one. Absent keeps the row live, which
|
|
1586
|
+
// is what a row written before the annotation is.
|
|
1587
|
+
let retiredExpr = switch (retiredField, retiredValues) {
|
|
1588
|
+
| (None, _) => "false"
|
|
1589
|
+
| (Some(f), Some(values)) =>
|
|
1590
|
+
let literal = values->Array.map(v => `'${v}'`)->Array.join(", ")
|
|
1591
|
+
`[${literal}].indexOf(row['${f}']) >= 0`
|
|
1592
|
+
| (Some(f), None) => `row['${f}'] === true`
|
|
1593
|
+
}
|
|
1594
|
+
// Only the state form has a state to name, and only a retired row reports one:
|
|
1595
|
+
// this door names rows, it does not publish a lifecycle column to callers the
|
|
1596
|
+
// list withholds.
|
|
1597
|
+
let stateExpr = switch (retiredField, retiredValues) {
|
|
1598
|
+
| (Some(f), Some(_)) => `_retired(row) ? (row['${f}'] ?? null) : null`
|
|
1599
|
+
| _ => "null"
|
|
1600
|
+
}
|
|
1601
|
+
`${importUtil}
|
|
1463
1602
|
import { runtime } from '@aws-appsync/utils';
|
|
1464
1603
|
export function request(ctx) {
|
|
1465
1604
|
const ids = ctx.args.ids ?? [];
|
|
@@ -1484,19 +1623,19 @@ export function response(ctx) {
|
|
|
1484
1623
|
.map(row => ({
|
|
1485
1624
|
id: row.id,
|
|
1486
1625
|
label: row['${labelField}'] ?? row.id,
|
|
1626
|
+
image: ${imageExpr->Option.getOr("null")},
|
|
1487
1627
|
retired: _retired(row),
|
|
1488
1628
|
retiredState: ${stateExpr},
|
|
1489
1629
|
}));
|
|
1490
1630
|
}
|
|
1491
1631
|
`
|
|
1492
|
-
}
|
|
1632
|
+
}
|
|
1493
1633
|
|
|
1494
1634
|
// ---------------------------------------------------------------------------
|
|
1495
1635
|
// DynamoDB write
|
|
1496
1636
|
// ---------------------------------------------------------------------------
|
|
1497
1637
|
|
|
1498
|
-
let putItem =
|
|
1499
|
-
`${importUtil}
|
|
1638
|
+
let putItem = `${importUtil}
|
|
1500
1639
|
export function request(ctx) {
|
|
1501
1640
|
return {
|
|
1502
1641
|
operation: 'PutItem',
|
|
@@ -1523,8 +1662,7 @@ export function request(ctx) {
|
|
|
1523
1662
|
${resultResponseCode}
|
|
1524
1663
|
`->Pulumi.Input.make
|
|
1525
1664
|
|
|
1526
|
-
let deleteItem =
|
|
1527
|
-
`${importUtil}
|
|
1665
|
+
let deleteItem = `${importUtil}
|
|
1528
1666
|
export function request(ctx) {
|
|
1529
1667
|
return {
|
|
1530
1668
|
operation: 'DeleteItem',
|
|
@@ -1538,6 +1676,24 @@ ${resultResponseCode}
|
|
|
1538
1676
|
// Lambda invocations
|
|
1539
1677
|
// ---------------------------------------------------------------------------
|
|
1540
1678
|
|
|
1679
|
+
// The caller as a command Lambda reads it: a Cognito user, an IAM principal, or null.
|
|
1680
|
+
let callerIdentity = `identity: isCognito
|
|
1681
|
+
? {
|
|
1682
|
+
userId: id.sub,
|
|
1683
|
+
username: id.username,
|
|
1684
|
+
groups: id.claims?.['cognito:groups'] ?? [],
|
|
1685
|
+
claims: id.claims,
|
|
1686
|
+
provider: 'Cognito'
|
|
1687
|
+
}
|
|
1688
|
+
: id != null
|
|
1689
|
+
? {
|
|
1690
|
+
userArn: id.userArn ?? null,
|
|
1691
|
+
accountId: id.accountId ?? null,
|
|
1692
|
+
username: id.username ?? null,
|
|
1693
|
+
provider: 'IAM'
|
|
1694
|
+
}
|
|
1695
|
+
: null`
|
|
1696
|
+
|
|
1541
1697
|
/** Invoke a CommandGenerator Lambda. `command` is the Aggregate command name. */
|
|
1542
1698
|
let invokeCommandGenerator = (command: string) =>
|
|
1543
1699
|
`${importUtil}
|
|
@@ -1554,22 +1710,7 @@ export function request(ctx) {
|
|
|
1554
1710
|
user: id?.username ?? null,
|
|
1555
1711
|
info: ctx.info.parentTypeName + '.' + ctx.info.fieldName
|
|
1556
1712
|
},
|
|
1557
|
-
|
|
1558
|
-
? {
|
|
1559
|
-
userId: id.sub,
|
|
1560
|
-
username: id.username,
|
|
1561
|
-
groups: id.claims?.['cognito:groups'] ?? [],
|
|
1562
|
-
claims: id.claims,
|
|
1563
|
-
provider: 'Cognito'
|
|
1564
|
-
}
|
|
1565
|
-
: id != null
|
|
1566
|
-
? {
|
|
1567
|
-
userArn: id.userArn ?? null,
|
|
1568
|
-
accountId: id.accountId ?? null,
|
|
1569
|
-
username: id.username ?? null,
|
|
1570
|
-
provider: 'IAM'
|
|
1571
|
-
}
|
|
1572
|
-
: null
|
|
1713
|
+
${callerIdentity}
|
|
1573
1714
|
}
|
|
1574
1715
|
};
|
|
1575
1716
|
}
|
|
@@ -1592,22 +1733,7 @@ export function request(ctx) {
|
|
|
1592
1733
|
user: id?.username ?? null,
|
|
1593
1734
|
info: ctx.info.parentTypeName + '.' + ctx.info.fieldName
|
|
1594
1735
|
},
|
|
1595
|
-
|
|
1596
|
-
? {
|
|
1597
|
-
userId: id.sub,
|
|
1598
|
-
username: id.username,
|
|
1599
|
-
groups: id.claims?.['cognito:groups'] ?? [],
|
|
1600
|
-
claims: id.claims,
|
|
1601
|
-
provider: 'Cognito'
|
|
1602
|
-
}
|
|
1603
|
-
: id != null
|
|
1604
|
-
? {
|
|
1605
|
-
userArn: id.userArn ?? null,
|
|
1606
|
-
accountId: id.accountId ?? null,
|
|
1607
|
-
username: id.username ?? null,
|
|
1608
|
-
provider: 'IAM'
|
|
1609
|
-
}
|
|
1610
|
-
: null
|
|
1736
|
+
${callerIdentity}
|
|
1611
1737
|
}
|
|
1612
1738
|
};
|
|
1613
1739
|
}
|
|
@@ -1618,12 +1744,15 @@ ${resultResponseCode}
|
|
|
1618
1744
|
let invokeInboundTranslation = (fieldName: string) =>
|
|
1619
1745
|
`${importUtil}
|
|
1620
1746
|
export function request(ctx) {
|
|
1747
|
+
const id = ctx.identity;
|
|
1748
|
+
const isCognito = id != null && id.sub != null;
|
|
1621
1749
|
return {
|
|
1622
1750
|
operation: 'Invoke',
|
|
1623
1751
|
payload: {
|
|
1624
1752
|
__inboundTranslation: true,
|
|
1625
1753
|
fieldName: '${fieldName}',
|
|
1626
|
-
arguments: ctx.args
|
|
1754
|
+
arguments: ctx.args,
|
|
1755
|
+
${callerIdentity}
|
|
1627
1756
|
}
|
|
1628
1757
|
};
|
|
1629
1758
|
}
|
|
@@ -1667,8 +1796,7 @@ export function response(ctx) {
|
|
|
1667
1796
|
let authorizeIndexedAccessRequest = authorizeIndexedAccess
|
|
1668
1797
|
|
|
1669
1798
|
/** @deprecated The response check is now part of authorizeIndexedAccess. */
|
|
1670
|
-
let authorizeIndexedAccessResponse = (~group as _: string) =>
|
|
1671
|
-
resultResponseCode->Pulumi.Input.make
|
|
1799
|
+
let authorizeIndexedAccessResponse = (~group as _: string) => resultResponseCode->Pulumi.Input.make
|
|
1672
1800
|
|
|
1673
1801
|
// ---------------------------------------------------------------------------
|
|
1674
1802
|
// Response-only helpers
|