@reventlessdev/rescript-pulumi-aws 3.0.0-alpha.4 → 3.0.0-alpha.6
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 +14 -0
- package/package.json +1 -1
- package/src/AppSync/AppSync_Resolver_Functions.res +91 -20
- package/src/AppSync/AppSync_Resolver_Functions.res.mjs +62 -19
- package/src/Cloudwatch/Cloudwatch_EventRule.res +4 -0
- package/src/Cloudwatch/Cloudwatch_EventTarget.res +11 -0
- package/src/example/AlarmMailExample.res +34 -0
- package/src/example/AlarmMailExample.res.mjs +44 -0
- package/tests/AppSync_Resolver_FunctionsTest.mjs +78 -12
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.6 (2026-08-24)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **pulumi-aws:** match events by pattern and rewrite what a target receives ([8f94ef3](https://github.com/ReventlessDev/reventless-core/commit/8f94ef3c4f5d24f87c1331b8a63c35bd9c4a92b2))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.5 (2026-08-23)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **querydb:** serve the previous page instead of refusing it ([48341c9](https://github.com/ReventlessDev/reventless-core/commit/48341c9f61b5155b441deeb0edc9abf461948346))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.4 (2026-08-23)
|
|
7
21
|
|
|
8
22
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -563,6 +563,18 @@ saying whether a filter was pushed down.
|
|
|
563
563
|
let pageWindowBudget = (~filtered: string) =>
|
|
564
564
|
`(${filtered} ? (_first > 1000 ? _first : 1000) : _first + _from)`
|
|
565
565
|
|
|
566
|
+
/**
|
|
567
|
+
The same budget for the full-list door, which may also read backward.
|
|
568
|
+
|
|
569
|
+
A backward page is the slice `[_from, _upTo)` of the window, so the read has to
|
|
570
|
+
reach `_upTo` rows into it — `_first + _from` would stop short and hand back a
|
|
571
|
+
page with its tail missing.
|
|
572
|
+
*/
|
|
573
|
+
let listPageWindowBudget = {
|
|
574
|
+
let need = "(_backward ? _upTo : _first + _from)"
|
|
575
|
+
`(parts.length > 0 ? (${need} > 1000 ? ${need} : 1000) : ${need})`
|
|
576
|
+
}
|
|
577
|
+
|
|
566
578
|
/**
|
|
567
579
|
Decodes `after` into the window it names (`t`, the token opening it) and the row's
|
|
568
580
|
index among that window's matches (`n`). Pre-window cursors carried
|
|
@@ -597,21 +609,74 @@ let cursorPathGuard = `
|
|
|
597
609
|
util.error('This cursor belongs to a different read of this list; restart from the first page.', 'CursorPathMismatch');
|
|
598
610
|
}`
|
|
599
611
|
|
|
612
|
+
/**
|
|
613
|
+
The full-list door's cursor decode, which unlike `cursorDecode` may run backward.
|
|
614
|
+
|
|
615
|
+
A `{t, n}` cursor names a position INSIDE a read window, and a window is re-read
|
|
616
|
+
from its own token — which forward paging already relies on to resume mid-window.
|
|
617
|
+
Backward is the same move in the other direction: re-read the window `before`
|
|
618
|
+
names and cut the page that ends at `n`, `[n - first, n)`. No new cursor shape, no
|
|
619
|
+
index, and no assumption forward paging does not already make.
|
|
620
|
+
|
|
621
|
+
What it cannot do is cross into an EARLIER window: DynamoDB's continuation chain
|
|
622
|
+
walks one way, so a window cannot name the one before it. `_upTo <= 0` with a
|
|
623
|
+
non-null window is that case, and it is the only one still refused.
|
|
624
|
+
|
|
625
|
+
Declares `_first` (the response's slicing needs it before `connectionPageResponse`
|
|
626
|
+
would have declared it, so that one omits it here).
|
|
627
|
+
*/
|
|
628
|
+
let listCursorPreamble = `
|
|
629
|
+
const _first = ctx.args.first ?? 50;
|
|
630
|
+
let _window = null;
|
|
631
|
+
let _from = 0;
|
|
632
|
+
let _cursorPath = null;
|
|
633
|
+
let _backward = false;
|
|
634
|
+
let _upTo = -1;
|
|
635
|
+
if (ctx.args.before != null && ctx.args.before !== '') {
|
|
636
|
+
const _c = JSON.parse(util.base64Decode(ctx.args.before));
|
|
637
|
+
_window = (_c.t !== undefined ? _c.t : _c.token) ?? null;
|
|
638
|
+
_cursorPath = _c.p ?? 's';
|
|
639
|
+
_backward = true;
|
|
640
|
+
_upTo = _c.n !== undefined ? _c.n : 0;
|
|
641
|
+
_from = (_upTo - _first) > 0 ? (_upTo - _first) : 0;
|
|
642
|
+
} else if (ctx.args.after != null && ctx.args.after !== '') {
|
|
643
|
+
const _c = JSON.parse(util.base64Decode(ctx.args.after));
|
|
644
|
+
_window = (_c.t !== undefined ? _c.t : _c.token) ?? null;
|
|
645
|
+
_from = _c.n !== undefined ? _c.n + 1 : 0;
|
|
646
|
+
_cursorPath = _c.p ?? 's';
|
|
647
|
+
}`
|
|
648
|
+
|
|
600
649
|
/**
|
|
601
650
|
Cuts the requested page out of the returned window. Expects `items` (sorted, if the
|
|
602
651
|
door sorts) plus `_window` / `_from` from `cursorDecode`. `pathExpr` is the JS
|
|
603
652
|
expression naming which read minted these cursors, for a door that has two.
|
|
653
|
+
|
|
654
|
+
`bidirectional` is the full-list door, whose page may have been cut backward: it
|
|
655
|
+
reads `_backward` / `_upTo` from `listCursorPreamble` and takes `_first` from
|
|
656
|
+
there rather than declaring its own.
|
|
604
657
|
*/
|
|
605
|
-
let connectionPageResponse = (~pathExpr: option<string
|
|
658
|
+
let connectionPageResponse = (~pathExpr: option<string>=?, ~bidirectional: bool=false) => {
|
|
606
659
|
let tag = switch pathExpr {
|
|
607
660
|
| None => ""
|
|
608
661
|
| Some(e) => `, p: ${e}`
|
|
609
662
|
}
|
|
610
|
-
|
|
611
|
-
|
|
663
|
+
let firstDecl = bidirectional ? "" : "\n const _first = ctx.args.first ?? 50;"
|
|
664
|
+
let slice = bidirectional
|
|
665
|
+
? `
|
|
666
|
+
const _rest = _backward ? items.slice(_from, _upTo) : items.slice(_from);
|
|
667
|
+
const _page = _backward ? _rest : _rest.slice(0, _first);
|
|
668
|
+
// A backward page was cut from ahead, so a next page provably exists — the one
|
|
669
|
+
// the caller came from — whatever this window's tail looks like.
|
|
670
|
+
const _more = _backward ? true : _rest.length > _first;`
|
|
671
|
+
: `
|
|
612
672
|
const _rest = items.slice(_from);
|
|
613
673
|
const _page = _rest.slice(0, _first);
|
|
614
|
-
const _more = _rest.length > _first
|
|
674
|
+
const _more = _rest.length > _first;`
|
|
675
|
+
// Only what the door can actually serve. `!!ctx.args.after` said "a page
|
|
676
|
+
// precedes this one", which is a different claim: at a window boundary one does
|
|
677
|
+
// and this door cannot reach it, so the client drew a Prev that always errored.
|
|
678
|
+
let hasPrevious = bidirectional ? "_from > 0" : "!!ctx.args.after"
|
|
679
|
+
`${firstDecl}${slice}
|
|
615
680
|
const _next = ctx.result?.nextToken ?? null;
|
|
616
681
|
const _lastIndex = _page.length - 1;
|
|
617
682
|
// A row's cursor names its own position. The last row of a page that closes its
|
|
@@ -632,7 +697,7 @@ let connectionPageResponse = (~pathExpr: option<string>=?) => {
|
|
|
632
697
|
edges,
|
|
633
698
|
pageInfo: {
|
|
634
699
|
hasNextPage: _more || !!_next,
|
|
635
|
-
hasPreviousPage:
|
|
700
|
+
hasPreviousPage: ${hasPrevious},
|
|
636
701
|
startCursor: edges.length > 0 ? edges[0].cursor : _boundary,
|
|
637
702
|
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : _boundary,
|
|
638
703
|
},
|
|
@@ -1039,7 +1104,7 @@ ${switch retiredValues {
|
|
|
1039
1104
|
const req = _exempt
|
|
1040
1105
|
? {
|
|
1041
1106
|
operation: 'Scan',
|
|
1042
|
-
limit: ${
|
|
1107
|
+
limit: ${listPageWindowBudget},
|
|
1043
1108
|
nextToken: _window,
|
|
1044
1109
|
}
|
|
1045
1110
|
: {
|
|
@@ -1050,14 +1115,14 @@ ${switch retiredValues {
|
|
|
1050
1115
|
expressionNames: { '#owner': '${field}' },
|
|
1051
1116
|
expressionValues: { ':owner': util.dynamodb.toDynamoDB(_sub) },
|
|
1052
1117
|
},
|
|
1053
|
-
limit: ${
|
|
1118
|
+
limit: ${listPageWindowBudget},
|
|
1054
1119
|
nextToken: _window,
|
|
1055
1120
|
scanIndexForward: !(_indexOrdered && ctx.args.orderBy.direction === 'DESC'),
|
|
1056
1121
|
};`
|
|
1057
1122
|
| _ => `
|
|
1058
1123
|
const req = {
|
|
1059
1124
|
operation: 'Scan',
|
|
1060
|
-
limit: ${
|
|
1125
|
+
limit: ${listPageWindowBudget},
|
|
1061
1126
|
nextToken: _window,
|
|
1062
1127
|
};`
|
|
1063
1128
|
}
|
|
@@ -1070,16 +1135,18 @@ ${switch retiredValues {
|
|
|
1070
1135
|
const _indexOrdered = ${indexOrderedExpr};`
|
|
1071
1136
|
}
|
|
1072
1137
|
let pageResponse = switch ownerIndex {
|
|
1073
|
-
| None => connectionPageResponse()
|
|
1074
|
-
| Some(_) => connectionPageResponse(~pathExpr="_path")
|
|
1138
|
+
| None => connectionPageResponse(~bidirectional=true)
|
|
1139
|
+
| Some(_) => connectionPageResponse(~pathExpr="_path", ~bidirectional=true)
|
|
1075
1140
|
}
|
|
1076
1141
|
`${importUtil}
|
|
1077
1142
|
export function request(ctx) {
|
|
1078
|
-
//
|
|
1079
|
-
//
|
|
1080
|
-
//
|
|
1081
|
-
|
|
1082
|
-
|
|
1143
|
+
// 'before' IS served — the page is cut backward out of the window the cursor
|
|
1144
|
+
// names. 'last' is not: "the last N rows of the list" needs the end of the
|
|
1145
|
+
// list, which a forward-only Scan cursor cannot reach. The ordered
|
|
1146
|
+
// {single}Items connection (queryItemsWithSortConditions) has a real keyset
|
|
1147
|
+
// cursor and honours both — direct callers who need 'last' there.
|
|
1148
|
+
if (ctx.args.last != null) {
|
|
1149
|
+
util.error('last is not supported on full-list connections; page backward with first and before.', 'UnsupportedPagination');
|
|
1083
1150
|
}
|
|
1084
1151
|
const filter = ctx.args.filter ?? {};
|
|
1085
1152
|
const names = {};
|
|
@@ -1104,8 +1171,14 @@ export function request(ctx) {
|
|
|
1104
1171
|
});
|
|
1105
1172
|
parts.push('#id IN (' + placeholders.join(', ') + ')');
|
|
1106
1173
|
}${filterClauses}${rangeClauses}${requireAttributeClause}${ownerClause}${retiredClause}
|
|
1107
|
-
${
|
|
1108
|
-
|
|
1174
|
+
${listCursorPreamble}${requestPathGuard}
|
|
1175
|
+
// The one page a cursor cannot reach: it begins in an earlier window, and a
|
|
1176
|
+
// continuation token cannot name the one before it. Refused by itself rather
|
|
1177
|
+
// than folded into the 'last' guard, because the two are different limits and a
|
|
1178
|
+
// caller can act on this one (page forward from the start).
|
|
1179
|
+
if (_backward && _upTo <= 0 && _window !== null) {
|
|
1180
|
+
util.error('The previous page begins in an earlier read window, which this cursor cannot name; page forward from the start.', 'UnsupportedPagination');
|
|
1181
|
+
}${requestOperation}
|
|
1109
1182
|
if (parts.length > 0) {
|
|
1110
1183
|
req.filter = {
|
|
1111
1184
|
expression: parts.join(' AND '),
|
|
@@ -1117,9 +1190,7 @@ ${cursorDecode(~args="ctx.args")}${requestPathGuard}
|
|
|
1117
1190
|
}
|
|
1118
1191
|
export function response(ctx) {
|
|
1119
1192
|
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
1120
|
-
let items = ctx.result?.items ?? [];${responsePathPreamble}${sortBlock}${
|
|
1121
|
-
~args="ctx.args",
|
|
1122
|
-
)}${pageResponse}
|
|
1193
|
+
let items = ctx.result?.items ?? [];${responsePathPreamble}${sortBlock}${listCursorPreamble}${pageResponse}
|
|
1123
1194
|
}
|
|
1124
1195
|
`->Pulumi.Input.make
|
|
1125
1196
|
}
|
|
@@ -392,6 +392,10 @@ function pageWindowBudget(filtered) {
|
|
|
392
392
|
return `(` + filtered + ` ? (_first > 1000 ? _first : 1000) : _first + _from)`;
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
let need = "(_backward ? _upTo : _first + _from)";
|
|
396
|
+
|
|
397
|
+
let listPageWindowBudget = `(parts.length > 0 ? (` + need + ` > 1000 ? ` + need + ` : 1000) : ` + need + `)`;
|
|
398
|
+
|
|
395
399
|
function cursorDecode(args) {
|
|
396
400
|
return `
|
|
397
401
|
let _window = null;
|
|
@@ -410,13 +414,42 @@ let cursorPathGuard = `
|
|
|
410
414
|
util.error('This cursor belongs to a different read of this list; restart from the first page.', 'CursorPathMismatch');
|
|
411
415
|
}`;
|
|
412
416
|
|
|
413
|
-
|
|
414
|
-
let tag = pathExpr !== undefined ? `, p: ` + pathExpr : "";
|
|
415
|
-
return `
|
|
417
|
+
let listCursorPreamble = `
|
|
416
418
|
const _first = ctx.args.first ?? 50;
|
|
419
|
+
let _window = null;
|
|
420
|
+
let _from = 0;
|
|
421
|
+
let _cursorPath = null;
|
|
422
|
+
let _backward = false;
|
|
423
|
+
let _upTo = -1;
|
|
424
|
+
if (ctx.args.before != null && ctx.args.before !== '') {
|
|
425
|
+
const _c = JSON.parse(util.base64Decode(ctx.args.before));
|
|
426
|
+
_window = (_c.t !== undefined ? _c.t : _c.token) ?? null;
|
|
427
|
+
_cursorPath = _c.p ?? 's';
|
|
428
|
+
_backward = true;
|
|
429
|
+
_upTo = _c.n !== undefined ? _c.n : 0;
|
|
430
|
+
_from = (_upTo - _first) > 0 ? (_upTo - _first) : 0;
|
|
431
|
+
} else if (ctx.args.after != null && ctx.args.after !== '') {
|
|
432
|
+
const _c = JSON.parse(util.base64Decode(ctx.args.after));
|
|
433
|
+
_window = (_c.t !== undefined ? _c.t : _c.token) ?? null;
|
|
434
|
+
_from = _c.n !== undefined ? _c.n + 1 : 0;
|
|
435
|
+
_cursorPath = _c.p ?? 's';
|
|
436
|
+
}`;
|
|
437
|
+
|
|
438
|
+
function connectionPageResponse(pathExpr, bidirectionalOpt) {
|
|
439
|
+
let bidirectional = bidirectionalOpt !== undefined ? bidirectionalOpt : false;
|
|
440
|
+
let tag = pathExpr !== undefined ? `, p: ` + pathExpr : "";
|
|
441
|
+
let firstDecl = bidirectional ? "" : "\n const _first = ctx.args.first ?? 50;";
|
|
442
|
+
let slice = bidirectional ? `
|
|
443
|
+
const _rest = _backward ? items.slice(_from, _upTo) : items.slice(_from);
|
|
444
|
+
const _page = _backward ? _rest : _rest.slice(0, _first);
|
|
445
|
+
// A backward page was cut from ahead, so a next page provably exists — the one
|
|
446
|
+
// the caller came from — whatever this window's tail looks like.
|
|
447
|
+
const _more = _backward ? true : _rest.length > _first;` : `
|
|
417
448
|
const _rest = items.slice(_from);
|
|
418
449
|
const _page = _rest.slice(0, _first);
|
|
419
|
-
const _more = _rest.length > _first
|
|
450
|
+
const _more = _rest.length > _first;`;
|
|
451
|
+
let hasPrevious = bidirectional ? "_from > 0" : "!!ctx.args.after";
|
|
452
|
+
return firstDecl + slice + `
|
|
420
453
|
const _next = ctx.result?.nextToken ?? null;
|
|
421
454
|
const _lastIndex = _page.length - 1;
|
|
422
455
|
// A row's cursor names its own position. The last row of a page that closes its
|
|
@@ -437,7 +470,7 @@ function connectionPageResponse(pathExpr) {
|
|
|
437
470
|
edges,
|
|
438
471
|
pageInfo: {
|
|
439
472
|
hasNextPage: _more || !!_next,
|
|
440
|
-
hasPreviousPage:
|
|
473
|
+
hasPreviousPage: ` + hasPrevious + `,
|
|
441
474
|
startCursor: edges.length > 0 ? edges[0].cursor : _boundary,
|
|
442
475
|
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : _boundary,
|
|
443
476
|
},
|
|
@@ -447,7 +480,7 @@ function connectionPageResponse(pathExpr) {
|
|
|
447
480
|
let indexConnectionResponseCode = `
|
|
448
481
|
export function response(ctx) {
|
|
449
482
|
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
450
|
-
const items = ctx.result?.items ?? [];` + cursorDecode("ctx.args") + connectionPageResponse(undefined) + `
|
|
483
|
+
const items = ctx.result?.items ?? [];` + cursorDecode("ctx.args") + connectionPageResponse(undefined, undefined) + `
|
|
451
484
|
}`;
|
|
452
485
|
|
|
453
486
|
let indexBackwardPagingGuard = `
|
|
@@ -686,7 +719,7 @@ function listAllItemsConnection(labelField, filterFieldsOpt, rangeFieldsOpt, sor
|
|
|
686
719
|
const req = _exempt
|
|
687
720
|
? {
|
|
688
721
|
operation: 'Scan',
|
|
689
|
-
limit: ` +
|
|
722
|
+
limit: ` + listPageWindowBudget + `,
|
|
690
723
|
nextToken: _window,
|
|
691
724
|
}
|
|
692
725
|
: {
|
|
@@ -697,33 +730,35 @@ function listAllItemsConnection(labelField, filterFieldsOpt, rangeFieldsOpt, sor
|
|
|
697
730
|
expressionNames: { '#owner': '` + ownerField + `' },
|
|
698
731
|
expressionValues: { ':owner': util.dynamodb.toDynamoDB(_sub) },
|
|
699
732
|
},
|
|
700
|
-
limit: ` +
|
|
733
|
+
limit: ` + listPageWindowBudget + `,
|
|
701
734
|
nextToken: _window,
|
|
702
735
|
scanIndexForward: !(_indexOrdered && ctx.args.orderBy.direction === 'DESC'),
|
|
703
736
|
};` : `
|
|
704
737
|
const req = {
|
|
705
738
|
operation: 'Scan',
|
|
706
|
-
limit: ` +
|
|
739
|
+
limit: ` + listPageWindowBudget + `,
|
|
707
740
|
nextToken: _window,
|
|
708
741
|
};`
|
|
709
742
|
) : `
|
|
710
743
|
const req = {
|
|
711
744
|
operation: 'Scan',
|
|
712
|
-
limit: ` +
|
|
745
|
+
limit: ` + listPageWindowBudget + `,
|
|
713
746
|
nextToken: _window,
|
|
714
747
|
};`;
|
|
715
748
|
let requestPathGuard = Stdlib_Option.isSome(ownerIndex$1) ? cursorPathGuard : "";
|
|
716
749
|
let responsePathPreamble = ownerIndex$1 !== undefined ? ownerIdentityPreamble + `
|
|
717
750
|
const _path = _exempt ? 's' : 'q';
|
|
718
751
|
const _indexOrdered = ` + indexOrderedExpr + `;` : "";
|
|
719
|
-
let pageResponse = ownerIndex$1 !== undefined ? connectionPageResponse("_path") : connectionPageResponse(undefined);
|
|
752
|
+
let pageResponse = ownerIndex$1 !== undefined ? connectionPageResponse("_path", true) : connectionPageResponse(undefined, true);
|
|
720
753
|
return importUtil + `
|
|
721
754
|
export function request(ctx) {
|
|
722
|
-
//
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
|
|
726
|
-
|
|
755
|
+
// 'before' IS served — the page is cut backward out of the window the cursor
|
|
756
|
+
// names. 'last' is not: "the last N rows of the list" needs the end of the
|
|
757
|
+
// list, which a forward-only Scan cursor cannot reach. The ordered
|
|
758
|
+
// {single}Items connection (queryItemsWithSortConditions) has a real keyset
|
|
759
|
+
// cursor and honours both — direct callers who need 'last' there.
|
|
760
|
+
if (ctx.args.last != null) {
|
|
761
|
+
util.error('last is not supported on full-list connections; page backward with first and before.', 'UnsupportedPagination');
|
|
727
762
|
}
|
|
728
763
|
const filter = ctx.args.filter ?? {};
|
|
729
764
|
const names = {};
|
|
@@ -748,8 +783,14 @@ export function request(ctx) {
|
|
|
748
783
|
});
|
|
749
784
|
parts.push('#id IN (' + placeholders.join(', ') + ')');
|
|
750
785
|
}` + filterClauses + rangeClauses + requireAttributeClause + ownerClause + retiredClause + `
|
|
751
|
-
` +
|
|
752
|
-
|
|
786
|
+
` + listCursorPreamble + requestPathGuard + `
|
|
787
|
+
// The one page a cursor cannot reach: it begins in an earlier window, and a
|
|
788
|
+
// continuation token cannot name the one before it. Refused by itself rather
|
|
789
|
+
// than folded into the 'last' guard, because the two are different limits and a
|
|
790
|
+
// caller can act on this one (page forward from the start).
|
|
791
|
+
if (_backward && _upTo <= 0 && _window !== null) {
|
|
792
|
+
util.error('The previous page begins in an earlier read window, which this cursor cannot name; page forward from the start.', 'UnsupportedPagination');
|
|
793
|
+
}` + requestOperation + `
|
|
753
794
|
if (parts.length > 0) {
|
|
754
795
|
req.filter = {
|
|
755
796
|
expression: parts.join(' AND '),
|
|
@@ -761,7 +802,7 @@ export function request(ctx) {
|
|
|
761
802
|
}
|
|
762
803
|
export function response(ctx) {
|
|
763
804
|
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
764
|
-
let items = ctx.result?.items ?? [];` + responsePathPreamble + sortBlock +
|
|
805
|
+
let items = ctx.result?.items ?? [];` + responsePathPreamble + sortBlock + listCursorPreamble + pageResponse + `
|
|
765
806
|
}
|
|
766
807
|
`;
|
|
767
808
|
}
|
|
@@ -1289,8 +1330,10 @@ export {
|
|
|
1289
1330
|
queryByIndexDeletable,
|
|
1290
1331
|
queryByIndexSort,
|
|
1291
1332
|
pageWindowBudget,
|
|
1333
|
+
listPageWindowBudget,
|
|
1292
1334
|
cursorDecode,
|
|
1293
1335
|
cursorPathGuard,
|
|
1336
|
+
listCursorPreamble,
|
|
1294
1337
|
connectionPageResponse,
|
|
1295
1338
|
indexConnectionResponseCode,
|
|
1296
1339
|
indexBackwardPagingGuard,
|
|
@@ -44,6 +44,10 @@ module ScheduleExpression: {
|
|
|
44
44
|
type args = {
|
|
45
45
|
description?: Pulumi.Input.t<string>,
|
|
46
46
|
scheduleExpression?: ScheduleExpression.t,
|
|
47
|
+
/** JSON event pattern the rule matches on. A rule fires on a schedule or on a
|
|
48
|
+
pattern — set exactly one of the two.
|
|
49
|
+
see: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html */
|
|
50
|
+
eventPattern?: Pulumi.Input.t<string>,
|
|
47
51
|
roleArn?: Pulumi.Input.t<string>,
|
|
48
52
|
tags?: Pulumi.Input.t<Aws.tags>,
|
|
49
53
|
}
|
|
@@ -17,10 +17,21 @@ module Rule: {
|
|
|
17
17
|
let ofEventRule = (rule: Cloudwatch_EventRule.t) => rule.name->Pulumi.Output.asInput
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** Rewrites a matched event before it reaches the target. `inputPaths` binds names to
|
|
21
|
+
JSON paths in the event; `inputTemplate` substitutes them where it writes `<name>`.
|
|
22
|
+
A template that is not JSON arrives as plain text — an SNS email body, say.
|
|
23
|
+
see: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html */
|
|
24
|
+
type inputTransformer = {
|
|
25
|
+
inputPaths?: dict<string>,
|
|
26
|
+
inputTemplate: Pulumi.Input.t<string>,
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
type args = {
|
|
21
30
|
rule: Rule.t,
|
|
22
31
|
arn: Pulumi.Input.t<string>,
|
|
32
|
+
/** A fixed payload, sent verbatim. Mutually exclusive with `inputTransformer`. */
|
|
23
33
|
input?: Pulumi.Input.t<string>,
|
|
34
|
+
inputTransformer?: Pulumi.Input.t<inputTransformer>,
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
@module("@pulumi/aws") @scope("cloudwatch") @new
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Compile-only smoke for the EventBridge bindings: a rule that matches CloudWatch
|
|
2
|
+
// alarm state changes by pattern, and an SNS target that rewrites the matched event
|
|
3
|
+
// into a plain-text body rather than forwarding the raw JSON. Not executed at deploy
|
|
4
|
+
// time — it keeps `eventPattern` and `inputTransformer` type-checked together.
|
|
5
|
+
|
|
6
|
+
let alarmMailTopic = SNS.Topic.make(~name="example-alarm-mail")
|
|
7
|
+
|
|
8
|
+
let alarmStateChanges = Cloudwatch.EventRule.make(
|
|
9
|
+
~name="example-alarm-state-changes",
|
|
10
|
+
~args={
|
|
11
|
+
description: "CloudWatch alarm state changes, ALARM and OK only"->Pulumi.Input.make,
|
|
12
|
+
eventPattern: `{"source":["aws.cloudwatch"],"detail-type":["CloudWatch Alarm State Change"],"detail":{"state":{"value":["ALARM","OK"]}}}`->Pulumi.Input.make,
|
|
13
|
+
},
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// A template that is not JSON has to arrive quoted — the surrounding double quotes are
|
|
17
|
+
// part of the value, not ReScript syntax.
|
|
18
|
+
let mailBody: Cloudwatch.EventTarget.inputTransformer = {
|
|
19
|
+
inputPaths: Dict.fromArray([
|
|
20
|
+
("name", "$.detail.alarmName"),
|
|
21
|
+
("state", "$.detail.state.value"),
|
|
22
|
+
("reason", "$.detail.state.reason"),
|
|
23
|
+
]),
|
|
24
|
+
inputTemplate: `"<state>: <name> — <reason>"`->Pulumi.Input.make,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let _ = Cloudwatch.EventTarget.make(
|
|
28
|
+
~name="example-alarm-mail-target",
|
|
29
|
+
~args={
|
|
30
|
+
rule: Cloudwatch.EventTarget.Rule.ofEventRule(alarmStateChanges),
|
|
31
|
+
arn: alarmMailTopic.arn->Pulumi.Output.asInput,
|
|
32
|
+
inputTransformer: mailBody->Pulumi.Input.make,
|
|
33
|
+
},
|
|
34
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Aws from "@pulumi/aws";
|
|
4
|
+
import * as Cloudwatch_EventTarget$PulumiAws from "../Cloudwatch/Cloudwatch_EventTarget.res.mjs";
|
|
5
|
+
|
|
6
|
+
let alarmMailTopic = new (Aws.sns.Topic)("example-alarm-mail");
|
|
7
|
+
|
|
8
|
+
let alarmStateChanges = new (Aws.cloudwatch.EventRule)("example-alarm-state-changes", {
|
|
9
|
+
description: "CloudWatch alarm state changes, ALARM and OK only",
|
|
10
|
+
eventPattern: `{"source":["aws.cloudwatch"],"detail-type":["CloudWatch Alarm State Change"],"detail":{"state":{"value":["ALARM","OK"]}}}`
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
let mailBody_inputPaths = Object.fromEntries([
|
|
14
|
+
[
|
|
15
|
+
"name",
|
|
16
|
+
"$.detail.alarmName"
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
"state",
|
|
20
|
+
"$.detail.state.value"
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
"reason",
|
|
24
|
+
"$.detail.state.reason"
|
|
25
|
+
]
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
let mailBody = {
|
|
29
|
+
inputPaths: mailBody_inputPaths,
|
|
30
|
+
inputTemplate: `"<state>: <name> — <reason>"`
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
new (Aws.cloudwatch.EventTarget)("example-alarm-mail-target", {
|
|
34
|
+
rule: Cloudwatch_EventTarget$PulumiAws.Rule.ofEventRule(alarmStateChanges),
|
|
35
|
+
arn: alarmMailTopic.arn,
|
|
36
|
+
inputTransformer: mailBody
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
alarmMailTopic,
|
|
41
|
+
alarmStateChanges,
|
|
42
|
+
mailBody,
|
|
43
|
+
}
|
|
44
|
+
/* alarmMailTopic Not a pure module */
|
|
@@ -209,6 +209,11 @@ describe('listAllItemsConnection', () => {
|
|
|
209
209
|
expect(req2.nextToken).toBe('TOK1')
|
|
210
210
|
})
|
|
211
211
|
|
|
212
|
+
// `hasPreviousPage` reports what this door can SERVE, not what exists. This
|
|
213
|
+
// cursor opens a new window at position 0, so the page before it lies in the
|
|
214
|
+
// previous window — real, and unreachable, because a continuation token
|
|
215
|
+
// cannot name the one before it. Claiming it is what drew a Prev button that
|
|
216
|
+
// always errored.
|
|
212
217
|
test('final page (nextToken null) closes the connection', () => {
|
|
213
218
|
const after = util.base64Encode(JSON.stringify({ t: 'TOK1', n: -1 }))
|
|
214
219
|
const r = response(makeCtx({
|
|
@@ -216,7 +221,7 @@ describe('listAllItemsConnection', () => {
|
|
|
216
221
|
result: { items: [{ id: 'z' }], nextToken: null },
|
|
217
222
|
}))
|
|
218
223
|
expect(r.pageInfo.hasNextPage).toBe(false)
|
|
219
|
-
expect(r.pageInfo.hasPreviousPage).toBe(
|
|
224
|
+
expect(r.pageInfo.hasPreviousPage).toBe(false)
|
|
220
225
|
})
|
|
221
226
|
|
|
222
227
|
// A filtered Scan can return an empty page while nextToken is still set. The
|
|
@@ -239,15 +244,55 @@ describe('listAllItemsConnection', () => {
|
|
|
239
244
|
expect(r.pageInfo.hasNextPage).toBe(false)
|
|
240
245
|
})
|
|
241
246
|
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
247
|
+
// `before` is served by re-reading the window the cursor names and cutting
|
|
248
|
+
// the page that ends at it. `last` is not: the last N rows of the list needs
|
|
249
|
+
// the END of the list, which a forward-only cursor cannot reach.
|
|
250
|
+
test('request rejects `last`, which needs the end of the list', () => {
|
|
251
|
+
expect(() => request(makeCtx({ args: { last: 5 } }))).toThrow('last is not supported')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('a backward page is the page that precedes it, exactly', () => {
|
|
255
|
+
const items = Array.from({ length: 25 }, (_, i) => ({ id: 'r' + i }))
|
|
256
|
+
const page1 = response(makeCtx({ args: { first: 10 }, result: { items, nextToken: null } }))
|
|
257
|
+
const fwd = { args: { first: 10, after: page1.pageInfo.endCursor } }
|
|
258
|
+
const page2 = response(makeCtx({ ...fwd, result: { items, nextToken: null } }))
|
|
259
|
+
expect(page2.edges.map(e => e.node.id)).toEqual(items.slice(10, 20).map(i => i.id))
|
|
260
|
+
|
|
261
|
+
const back = { args: { first: 10, before: page2.pageInfo.startCursor } }
|
|
262
|
+
const prev = response(makeCtx({ ...back, result: { items, nextToken: null } }))
|
|
263
|
+
expect(prev.edges.map(e => e.node.id)).toEqual(page1.edges.map(e => e.node.id))
|
|
264
|
+
// Cut from ahead, so a next page provably exists — the one we came from.
|
|
265
|
+
expect(prev.pageInfo.hasNextPage).toBe(true)
|
|
266
|
+
expect(prev.pageInfo.hasPreviousPage).toBe(false)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// The read must reach `_upTo` rows into the window, not `first + from`, or the
|
|
270
|
+
// backward slice comes back with its tail missing.
|
|
271
|
+
test('a backward read examines the window as deep as the page it cuts', () => {
|
|
272
|
+
const before = util.base64Encode(JSON.stringify({ t: null, n: 30 }))
|
|
273
|
+
expect(request(makeCtx({ args: { first: 10, before } })).limit).toBe(30)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('the window a backward cursor names is the one re-read', () => {
|
|
277
|
+
const before = util.base64Encode(JSON.stringify({ t: 'W7', n: 30 }))
|
|
278
|
+
expect(request(makeCtx({ args: { first: 10, before } })).nextToken).toBe('W7')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// The one page still out of reach, and the only case left refused.
|
|
282
|
+
test('a previous page in an earlier window is refused, and says which limit', () => {
|
|
283
|
+
const before = util.base64Encode(JSON.stringify({ t: 'W2', n: 0 }))
|
|
284
|
+
expect(() => request(makeCtx({ args: { first: 10, before } }))).toThrow(
|
|
285
|
+
'earlier read window',
|
|
246
286
|
)
|
|
247
287
|
})
|
|
248
288
|
|
|
249
|
-
|
|
250
|
-
|
|
289
|
+
// At the very start there is no earlier window to be unable to name, so a
|
|
290
|
+
// short backward page is served rather than refused.
|
|
291
|
+
test('a backward page at the start of the first window is served, not refused', () => {
|
|
292
|
+
const items = Array.from({ length: 10 }, (_, i) => ({ id: 'r' + i }))
|
|
293
|
+
const before = util.base64Encode(JSON.stringify({ t: null, n: 3 }))
|
|
294
|
+
const r = response(makeCtx({ args: { first: 10, before }, result: { items, nextToken: null } }))
|
|
295
|
+
expect(r.edges.map(e => e.node.id)).toEqual(['r0', 'r1', 'r2'])
|
|
251
296
|
})
|
|
252
297
|
|
|
253
298
|
// Cursors minted before the read window existed named the following window.
|
|
@@ -1025,16 +1070,37 @@ describe('listAllItemsConnection — owner scoping', () => {
|
|
|
1025
1070
|
})
|
|
1026
1071
|
})
|
|
1027
1072
|
|
|
1028
|
-
// Backward paging
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1073
|
+
// Backward paging works the same way on both branches, and for the same reason:
|
|
1074
|
+
// it re-reads the window the cursor names and cuts the page ending at it, which
|
|
1075
|
+
// is operation-agnostic. `last` stays refused on both — it needs the end of the
|
|
1076
|
+
// list, which neither a Scan nor this Query's cursor can reach.
|
|
1031
1077
|
test.each([['scoped', asUser('cust-a')], ['exempt', asUser('ops-1', ['Admin'])]])(
|
|
1032
|
-
'a %s caller is still refused
|
|
1078
|
+
'a %s caller is still refused `last`',
|
|
1033
1079
|
(_, identity) => {
|
|
1034
1080
|
const { request } = indexed()
|
|
1035
|
-
expect(() => request(makeCtx({ args: { last: 5 }, identity }))).toThrow(
|
|
1081
|
+
expect(() => request(makeCtx({ args: { last: 5 }, identity }))).toThrow(
|
|
1082
|
+
'last is not supported',
|
|
1083
|
+
)
|
|
1036
1084
|
},
|
|
1037
1085
|
)
|
|
1086
|
+
|
|
1087
|
+
// The path tag has to survive the direction change: a backward cursor still
|
|
1088
|
+
// names the read that minted it, and replaying it on the other branch is the
|
|
1089
|
+
// same mistake as replaying a forward one.
|
|
1090
|
+
test('a backward cursor is path-checked like a forward one', () => {
|
|
1091
|
+
const { request, response } = indexed()
|
|
1092
|
+
const scopedPage = response(makeCtx({
|
|
1093
|
+
args: { first: 10 },
|
|
1094
|
+
identity: asUser('cust-a'),
|
|
1095
|
+
result: { items: Array.from({ length: 10 }, (_, i) => ({ id: 'r' + i })), nextToken: null },
|
|
1096
|
+
}))
|
|
1097
|
+
expect(() =>
|
|
1098
|
+
request(makeCtx({
|
|
1099
|
+
args: { first: 10, before: scopedPage.pageInfo.endCursor },
|
|
1100
|
+
identity: asUser('ops-1', ['Admin']),
|
|
1101
|
+
})),
|
|
1102
|
+
).toThrow('different read of this list')
|
|
1103
|
+
})
|
|
1038
1104
|
})
|
|
1039
1105
|
|
|
1040
1106
|
// ---------------------------------------------------------------------------
|