@reventlessdev/rescript-pulumi-aws 3.0.0-alpha.26 → 3.0.0-alpha.27
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
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
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.27 (2026-10-10)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **pulumi-aws:** a list's last page no longer promises another ([00b141f](https://github.com/ReventlessDev/reventless-core/commit/00b141fbfdbf44544fbea8ab979356831381f48a))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# 3.0.0-alpha.26 (2026-10-10)
|
|
7
14
|
|
|
8
15
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/rescript-pulumi-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.27",
|
|
4
4
|
"description": "ReScript bindings for @pulumi/aws",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"@pulumi/aws-native": "^1.62.0",
|
|
24
24
|
"sury": "11.0.0",
|
|
25
25
|
"@reventlessdev/rescript-node": "2.0.0-alpha.22",
|
|
26
|
-
"@reventlessdev/rescript-
|
|
27
|
-
"@reventlessdev/rescript-
|
|
26
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.30",
|
|
27
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"esbuild": "^0.25.12",
|
|
@@ -564,19 +564,24 @@ the survivors serves short — usually empty — pages under a selective filter.
|
|
|
564
564
|
no loops in APPSYNC_JS the door reads wider instead and addresses the surplus by
|
|
565
565
|
position. 1 MB caps a page anyway, hence 1000; `filtered` is the JS expression
|
|
566
566
|
saying whether a filter was pushed down.
|
|
567
|
+
|
|
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.
|
|
567
571
|
*/
|
|
568
572
|
let pageWindowBudget = (~filtered: string) =>
|
|
569
|
-
`(${filtered} ? (_first > 1000 ? _first : 1000) : _first + _from)`
|
|
573
|
+
`(${filtered} ? (_first > 1000 ? _first : 1000) : _first + _from + 1)`
|
|
570
574
|
|
|
571
575
|
/**
|
|
572
576
|
The same budget for the full-list door, which may also read backward.
|
|
573
577
|
|
|
574
578
|
A backward page is the slice `[_from, _upTo)` of the window, so the read has to
|
|
575
579
|
reach `_upTo` rows into it — `_first + _from` would stop short and hand back a
|
|
576
|
-
page with its tail missing.
|
|
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.
|
|
577
582
|
*/
|
|
578
583
|
let listPageWindowBudget = {
|
|
579
|
-
let need = "(_backward ? _upTo : _first + _from)"
|
|
584
|
+
let need = "(_backward ? _upTo : _first + _from + 1)"
|
|
580
585
|
`(parts.length > 0 ? (${need} > 1000 ? ${need} : 1000) : ${need})`
|
|
581
586
|
}
|
|
582
587
|
|
|
@@ -389,10 +389,10 @@ export function request(ctx) {
|
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
function pageWindowBudget(filtered) {
|
|
392
|
-
return `(` + filtered + ` ? (_first > 1000 ? _first : 1000) : _first + _from)`;
|
|
392
|
+
return `(` + filtered + ` ? (_first > 1000 ? _first : 1000) : _first + _from + 1)`;
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
let need = "(_backward ? _upTo : _first + _from)";
|
|
395
|
+
let need = "(_backward ? _upTo : _first + _from + 1)";
|
|
396
396
|
|
|
397
397
|
let listPageWindowBudget = `(parts.length > 0 ? (` + need + ` > 1000 ? ` + need + ` : 1000) : ` + need + `)`;
|
|
398
398
|
|
|
@@ -528,7 +528,7 @@ export function request(ctx) {
|
|
|
528
528
|
operation: 'Query',
|
|
529
529
|
query,
|
|
530
530
|
index: '` + index + `',
|
|
531
|
-
limit: ` + (`(` + "expression" + ` ? (_first > 1000 ? _first : 1000) : _first + _from)`) + `,
|
|
531
|
+
limit: ` + (`(` + "expression" + ` ? (_first > 1000 ? _first : 1000) : _first + _from + 1)`) + `,
|
|
532
532
|
nextToken: _window,
|
|
533
533
|
scanIndexForward: (args.forward ?? true)
|
|
534
534
|
};
|
|
@@ -585,7 +585,7 @@ export function request(ctx) {
|
|
|
585
585
|
operation: 'Query',
|
|
586
586
|
query,
|
|
587
587
|
index: '` + index + `',
|
|
588
|
-
limit: ` + (`(` + "expression" + ` ? (_first > 1000 ? _first : 1000) : _first + _from)`) + `,
|
|
588
|
+
limit: ` + (`(` + "expression" + ` ? (_first > 1000 ? _first : 1000) : _first + _from + 1)`) + `,
|
|
589
589
|
nextToken: _window,
|
|
590
590
|
scanIndexForward: (args.forward ?? true)
|
|
591
591
|
};
|
|
@@ -158,7 +158,7 @@ describe('listAllItemsConnection', () => {
|
|
|
158
158
|
const ctx = makeCtx({ args: {} })
|
|
159
159
|
const result = request(ctx)
|
|
160
160
|
expect(result.operation).toBe('Scan')
|
|
161
|
-
expect(result.limit).toBe(
|
|
161
|
+
expect(result.limit).toBe(51)
|
|
162
162
|
expect(result.nextToken).toBeNull()
|
|
163
163
|
expect(result.filter).toBeUndefined()
|
|
164
164
|
})
|
|
@@ -317,10 +317,11 @@ describe('listAllItemsConnection', () => {
|
|
|
317
317
|
F.listAllItemsConnection('name', [], [], [], undefined, 'customerId', ['Admin']),
|
|
318
318
|
)
|
|
319
319
|
|
|
320
|
-
|
|
320
|
+
// One row past the page: it is what says a next page exists.
|
|
321
|
+
test('an unfiltered read examines one row past the page it serves', () => {
|
|
321
322
|
const req = request(makeCtx({ args: { first: 25 }, identity: null }))
|
|
322
323
|
expect(req.filter).toBeUndefined()
|
|
323
|
-
expect(req.limit).toBe(
|
|
324
|
+
expect(req.limit).toBe(26)
|
|
324
325
|
})
|
|
325
326
|
|
|
326
327
|
test('a filtered read examines a wider window than the page', () => {
|
|
@@ -330,7 +331,7 @@ describe('listAllItemsConnection', () => {
|
|
|
330
331
|
})
|
|
331
332
|
|
|
332
333
|
test('a caller asking for more than the window gets what it asked for', () => {
|
|
333
|
-
expect(request(makeCtx({ args: { first: 5000 } })).limit).toBe(
|
|
334
|
+
expect(request(makeCtx({ args: { first: 5000 } })).limit).toBe(5001)
|
|
334
335
|
})
|
|
335
336
|
|
|
336
337
|
// The user-visible bug: one matching row must be one page, not three.
|
|
@@ -373,6 +374,34 @@ describe('listAllItemsConnection', () => {
|
|
|
373
374
|
).toBe('W2')
|
|
374
375
|
})
|
|
375
376
|
|
|
377
|
+
// DynamoDB hands back a token whenever a read stops AT its limit, whether or
|
|
378
|
+
// not anything follows. A read of exactly the page therefore claimed a next
|
|
379
|
+
// page after the last full one, and it came back blank — found on a deployed
|
|
380
|
+
// list paged one row at a time. The mock pages a table the way DynamoDB does.
|
|
381
|
+
test('paging to the end serves no blank page, whatever the page size', () => {
|
|
382
|
+
const rows = ['a', 'b', 'c', 'd'].map(id => ({ id }))
|
|
383
|
+
const dynamo = req => {
|
|
384
|
+
const start = req.nextToken === null ? 0 : +req.nextToken
|
|
385
|
+
const items = rows.slice(start, start + req.limit)
|
|
386
|
+
const stoppedAtLimit = items.length === req.limit
|
|
387
|
+
return { items, nextToken: stoppedAtLimit ? String(start + req.limit) : null }
|
|
388
|
+
}
|
|
389
|
+
;[1, 2, 4].map(first => {
|
|
390
|
+
const pages = []
|
|
391
|
+
let after
|
|
392
|
+
for (let i = 0; i < 10; i++) {
|
|
393
|
+
const args = after === undefined ? { first } : { first, after }
|
|
394
|
+
const r = response(makeCtx({ args, result: dynamo(request(makeCtx({ args, identity: null }))) }))
|
|
395
|
+
pages.push(r.edges.map(e => e.node.id))
|
|
396
|
+
if (!r.pageInfo.hasNextPage) break
|
|
397
|
+
after = r.pageInfo.endCursor
|
|
398
|
+
}
|
|
399
|
+
expect(pages.flat()).toEqual(['a', 'b', 'c', 'd'])
|
|
400
|
+
expect(pages.every(p => p.length > 0)).toBe(true)
|
|
401
|
+
expect(pages).toHaveLength(Math.ceil(4 / first))
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
376
405
|
// Without this the final row of a window resumes into a window with nothing
|
|
377
406
|
// left in it, which is the blank page again one step later.
|
|
378
407
|
test('the last row of a page that closes its window names the next window', () => {
|
|
@@ -587,7 +616,7 @@ describe('queryByIndexSortFiltered', () => {
|
|
|
587
616
|
expect(result.query.expression).toBe('#ownerId = :ownerId')
|
|
588
617
|
expect(result.filter).toBeUndefined()
|
|
589
618
|
expect(result.index).toBe('ownerId')
|
|
590
|
-
expect(result.limit).toBe(
|
|
619
|
+
expect(result.limit).toBe(51)
|
|
591
620
|
})
|
|
592
621
|
|
|
593
622
|
test('query includes sortField when present', () => {
|
|
@@ -974,11 +1003,11 @@ describe('listAllItemsConnection — owner scoping', () => {
|
|
|
974
1003
|
|
|
975
1004
|
// The read window exists because a FilterExpression cuts rows AFTER `Limit`.
|
|
976
1005
|
// A key condition does not, so a scoped caller with no filter of their own
|
|
977
|
-
// examines
|
|
978
|
-
// hiding, gone rather than papered over.
|
|
979
|
-
test('a scoped caller with no filter examines
|
|
1006
|
+
// examines the page they asked for and the one row that says whether another
|
|
1007
|
+
// follows — the arithmetic the window was hiding, gone rather than papered over.
|
|
1008
|
+
test('a scoped caller with no filter examines only the page, plus one', () => {
|
|
980
1009
|
const { request } = indexed()
|
|
981
|
-
expect(request(makeCtx({ args: { first: 25 }, identity: asUser('cust-a') })).limit).toBe(
|
|
1010
|
+
expect(request(makeCtx({ args: { first: 25 }, identity: asUser('cust-a') })).limit).toBe(26)
|
|
982
1011
|
})
|
|
983
1012
|
|
|
984
1013
|
test("a scoped caller's own filter still widens the window", () => {
|