@reventlessdev/rescript-pulumi-aws 3.0.0-alpha.25 → 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,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.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
|
+
|
|
13
|
+
# 3.0.0-alpha.26 (2026-10-10)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* an inbound translation is gated alike on every door, and encodes its commands by schema ([6ec3ce6](https://github.com/ReventlessDev/reventless-core/commit/6ec3ce672e53e2c162c64a90d103be691391be5e))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.25 (2026-10-08)
|
|
7
21
|
|
|
8
22
|
**Note:** Version bump only for package @reventlessdev/rescript-pulumi-aws
|
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": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"@pulumi/aws": "~7.19.0",
|
|
23
23
|
"@pulumi/aws-native": "^1.62.0",
|
|
24
24
|
"sury": "11.0.0",
|
|
25
|
-
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.30",
|
|
26
25
|
"@reventlessdev/rescript-node": "2.0.0-alpha.22",
|
|
26
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.30",
|
|
27
27
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
@@ -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
|
|
|
@@ -1671,6 +1676,24 @@ ${resultResponseCode}
|
|
|
1671
1676
|
// Lambda invocations
|
|
1672
1677
|
// ---------------------------------------------------------------------------
|
|
1673
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
|
+
|
|
1674
1697
|
/** Invoke a CommandGenerator Lambda. `command` is the Aggregate command name. */
|
|
1675
1698
|
let invokeCommandGenerator = (command: string) =>
|
|
1676
1699
|
`${importUtil}
|
|
@@ -1687,22 +1710,7 @@ export function request(ctx) {
|
|
|
1687
1710
|
user: id?.username ?? null,
|
|
1688
1711
|
info: ctx.info.parentTypeName + '.' + ctx.info.fieldName
|
|
1689
1712
|
},
|
|
1690
|
-
|
|
1691
|
-
? {
|
|
1692
|
-
userId: id.sub,
|
|
1693
|
-
username: id.username,
|
|
1694
|
-
groups: id.claims?.['cognito:groups'] ?? [],
|
|
1695
|
-
claims: id.claims,
|
|
1696
|
-
provider: 'Cognito'
|
|
1697
|
-
}
|
|
1698
|
-
: id != null
|
|
1699
|
-
? {
|
|
1700
|
-
userArn: id.userArn ?? null,
|
|
1701
|
-
accountId: id.accountId ?? null,
|
|
1702
|
-
username: id.username ?? null,
|
|
1703
|
-
provider: 'IAM'
|
|
1704
|
-
}
|
|
1705
|
-
: null
|
|
1713
|
+
${callerIdentity}
|
|
1706
1714
|
}
|
|
1707
1715
|
};
|
|
1708
1716
|
}
|
|
@@ -1725,22 +1733,7 @@ export function request(ctx) {
|
|
|
1725
1733
|
user: id?.username ?? null,
|
|
1726
1734
|
info: ctx.info.parentTypeName + '.' + ctx.info.fieldName
|
|
1727
1735
|
},
|
|
1728
|
-
|
|
1729
|
-
? {
|
|
1730
|
-
userId: id.sub,
|
|
1731
|
-
username: id.username,
|
|
1732
|
-
groups: id.claims?.['cognito:groups'] ?? [],
|
|
1733
|
-
claims: id.claims,
|
|
1734
|
-
provider: 'Cognito'
|
|
1735
|
-
}
|
|
1736
|
-
: id != null
|
|
1737
|
-
? {
|
|
1738
|
-
userArn: id.userArn ?? null,
|
|
1739
|
-
accountId: id.accountId ?? null,
|
|
1740
|
-
username: id.username ?? null,
|
|
1741
|
-
provider: 'IAM'
|
|
1742
|
-
}
|
|
1743
|
-
: null
|
|
1736
|
+
${callerIdentity}
|
|
1744
1737
|
}
|
|
1745
1738
|
};
|
|
1746
1739
|
}
|
|
@@ -1751,12 +1744,15 @@ ${resultResponseCode}
|
|
|
1751
1744
|
let invokeInboundTranslation = (fieldName: string) =>
|
|
1752
1745
|
`${importUtil}
|
|
1753
1746
|
export function request(ctx) {
|
|
1747
|
+
const id = ctx.identity;
|
|
1748
|
+
const isCognito = id != null && id.sub != null;
|
|
1754
1749
|
return {
|
|
1755
1750
|
operation: 'Invoke',
|
|
1756
1751
|
payload: {
|
|
1757
1752
|
__inboundTranslation: true,
|
|
1758
1753
|
fieldName: '${fieldName}',
|
|
1759
|
-
arguments: ctx.args
|
|
1754
|
+
arguments: ctx.args,
|
|
1755
|
+
${callerIdentity}
|
|
1760
1756
|
}
|
|
1761
1757
|
};
|
|
1762
1758
|
}
|
|
@@ -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
|
};
|
|
@@ -1140,6 +1140,23 @@ export function request(ctx) {
|
|
|
1140
1140
|
` + resultResponseCode + `
|
|
1141
1141
|
`;
|
|
1142
1142
|
|
|
1143
|
+
let callerIdentity = `identity: isCognito
|
|
1144
|
+
? {
|
|
1145
|
+
userId: id.sub,
|
|
1146
|
+
username: id.username,
|
|
1147
|
+
groups: id.claims?.['cognito:groups'] ?? [],
|
|
1148
|
+
claims: id.claims,
|
|
1149
|
+
provider: 'Cognito'
|
|
1150
|
+
}
|
|
1151
|
+
: id != null
|
|
1152
|
+
? {
|
|
1153
|
+
userArn: id.userArn ?? null,
|
|
1154
|
+
accountId: id.accountId ?? null,
|
|
1155
|
+
username: id.username ?? null,
|
|
1156
|
+
provider: 'IAM'
|
|
1157
|
+
}
|
|
1158
|
+
: null`;
|
|
1159
|
+
|
|
1143
1160
|
function invokeCommandGenerator(command) {
|
|
1144
1161
|
return importUtil + `
|
|
1145
1162
|
export function request(ctx) {
|
|
@@ -1155,22 +1172,7 @@ export function request(ctx) {
|
|
|
1155
1172
|
user: id?.username ?? null,
|
|
1156
1173
|
info: ctx.info.parentTypeName + '.' + ctx.info.fieldName
|
|
1157
1174
|
},
|
|
1158
|
-
|
|
1159
|
-
? {
|
|
1160
|
-
userId: id.sub,
|
|
1161
|
-
username: id.username,
|
|
1162
|
-
groups: id.claims?.['cognito:groups'] ?? [],
|
|
1163
|
-
claims: id.claims,
|
|
1164
|
-
provider: 'Cognito'
|
|
1165
|
-
}
|
|
1166
|
-
: id != null
|
|
1167
|
-
? {
|
|
1168
|
-
userArn: id.userArn ?? null,
|
|
1169
|
-
accountId: id.accountId ?? null,
|
|
1170
|
-
username: id.username ?? null,
|
|
1171
|
-
provider: 'IAM'
|
|
1172
|
-
}
|
|
1173
|
-
: null
|
|
1175
|
+
` + callerIdentity + `
|
|
1174
1176
|
}
|
|
1175
1177
|
};
|
|
1176
1178
|
}
|
|
@@ -1193,22 +1195,7 @@ export function request(ctx) {
|
|
|
1193
1195
|
user: id?.username ?? null,
|
|
1194
1196
|
info: ctx.info.parentTypeName + '.' + ctx.info.fieldName
|
|
1195
1197
|
},
|
|
1196
|
-
|
|
1197
|
-
? {
|
|
1198
|
-
userId: id.sub,
|
|
1199
|
-
username: id.username,
|
|
1200
|
-
groups: id.claims?.['cognito:groups'] ?? [],
|
|
1201
|
-
claims: id.claims,
|
|
1202
|
-
provider: 'Cognito'
|
|
1203
|
-
}
|
|
1204
|
-
: id != null
|
|
1205
|
-
? {
|
|
1206
|
-
userArn: id.userArn ?? null,
|
|
1207
|
-
accountId: id.accountId ?? null,
|
|
1208
|
-
username: id.username ?? null,
|
|
1209
|
-
provider: 'IAM'
|
|
1210
|
-
}
|
|
1211
|
-
: null
|
|
1198
|
+
` + callerIdentity + `
|
|
1212
1199
|
}
|
|
1213
1200
|
};
|
|
1214
1201
|
}
|
|
@@ -1219,12 +1206,15 @@ export function request(ctx) {
|
|
|
1219
1206
|
function invokeInboundTranslation(fieldName) {
|
|
1220
1207
|
return importUtil + `
|
|
1221
1208
|
export function request(ctx) {
|
|
1209
|
+
const id = ctx.identity;
|
|
1210
|
+
const isCognito = id != null && id.sub != null;
|
|
1222
1211
|
return {
|
|
1223
1212
|
operation: 'Invoke',
|
|
1224
1213
|
payload: {
|
|
1225
1214
|
__inboundTranslation: true,
|
|
1226
1215
|
fieldName: '` + fieldName + `',
|
|
1227
|
-
arguments: ctx.args
|
|
1216
|
+
arguments: ctx.args,
|
|
1217
|
+
` + callerIdentity + `
|
|
1228
1218
|
}
|
|
1229
1219
|
};
|
|
1230
1220
|
}
|
|
@@ -1355,6 +1345,7 @@ export {
|
|
|
1355
1345
|
putItem,
|
|
1356
1346
|
addItemToList,
|
|
1357
1347
|
deleteItem,
|
|
1348
|
+
callerIdentity,
|
|
1358
1349
|
invokeCommandGenerator,
|
|
1359
1350
|
invokeDcbMutation,
|
|
1360
1351
|
invokeInboundTranslation,
|
|
@@ -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', () => {
|
|
@@ -757,6 +786,28 @@ describe('invokeInboundTranslation', () => {
|
|
|
757
786
|
expect(result.payload.fieldName).toBe('importProduct')
|
|
758
787
|
expect(result.payload.arguments).toEqual({ csv: 'data' })
|
|
759
788
|
})
|
|
789
|
+
|
|
790
|
+
// The Lambda checks each translated command against the caller's groups.
|
|
791
|
+
test('request carries the Cognito caller and its groups', () => {
|
|
792
|
+
const ctx = makeCtx({
|
|
793
|
+
identity: { username: 'alice', sub: 's1', sourceIp: [], claims: { 'cognito:groups': ['Merchandiser'] } },
|
|
794
|
+
})
|
|
795
|
+
const { payload } = request(ctx)
|
|
796
|
+
expect(payload.identity.userId).toBe('s1')
|
|
797
|
+
expect(payload.identity.groups).toEqual(['Merchandiser'])
|
|
798
|
+
expect(payload.identity.provider).toBe('Cognito')
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
test('request carries an IAM caller as IAM', () => {
|
|
802
|
+
const ctx = makeCtx({ identity: { userArn: 'arn:aws:iam::123:role/Sync', accountId: '123' } })
|
|
803
|
+
const { payload } = request(ctx)
|
|
804
|
+
expect(payload.identity.provider).toBe('IAM')
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
test('request emits null identity when ctx.identity is null', () => {
|
|
808
|
+
const { payload } = request(makeCtx({ identity: null }))
|
|
809
|
+
expect(payload.identity).toBeNull()
|
|
810
|
+
})
|
|
760
811
|
})
|
|
761
812
|
|
|
762
813
|
// ---------------------------------------------------------------------------
|
|
@@ -952,11 +1003,11 @@ describe('listAllItemsConnection — owner scoping', () => {
|
|
|
952
1003
|
|
|
953
1004
|
// The read window exists because a FilterExpression cuts rows AFTER `Limit`.
|
|
954
1005
|
// A key condition does not, so a scoped caller with no filter of their own
|
|
955
|
-
// examines
|
|
956
|
-
// hiding, gone rather than papered over.
|
|
957
|
-
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', () => {
|
|
958
1009
|
const { request } = indexed()
|
|
959
|
-
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)
|
|
960
1011
|
})
|
|
961
1012
|
|
|
962
1013
|
test("a scoped caller's own filter still widens the window", () => {
|