@reventlessdev/rescript-pulumi-aws 2.4.0-alpha.58 → 2.4.0-alpha.60
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 +28 -5
- package/src/AppSync/AppSync_Resolver_Functions.res.mjs +28 -5
- package/src/Location/Location.res +4 -0
- package/src/Location/Location.res.mjs +9 -0
- package/src/Location/Location_PlaceIndex.res +33 -0
- package/src/Location/Location_PlaceIndex.res.mjs +2 -0
- package/tests/AppSync_Resolver_FunctionsTest.mjs +56 -1
- package/tests/__mocks__/appsync-utils.mjs +2 -0
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
|
+
# 2.4.0-alpha.60 (2026-07-26)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **example:** activate geocoder + upload services in platform-aws deploy ([e684d66](https://github.com/ReventlessDev/reventless-core/commit/e684d66bc4ce844ae36e8292ed0b78fd965490ff))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 2.4.0-alpha.59 (2026-07-23)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **aws:** round-trip the DynamoDB Scan cursor in full-list connections ([0e21008](https://github.com/ReventlessDev/reventless-core/commit/0e2100860767a153a5c2f50a88fd6b501787c7f4))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 2.4.0-alpha.58 (2026-07-22)
|
|
7
21
|
|
|
8
22
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -506,6 +506,12 @@ let listAllItemsConnection = (
|
|
|
506
506
|
}
|
|
507
507
|
`${importUtil}
|
|
508
508
|
export function request(ctx) {
|
|
509
|
+
// Scan cannot page backward (ScanIndexForward is Query-only). Fail loud rather than
|
|
510
|
+
// silently returning the forward page. The ordered {single}Items connection
|
|
511
|
+
// (queryItemsWithSortConditions) supports last/before — direct backward callers there.
|
|
512
|
+
if (ctx.args.before != null || ctx.args.last != null) {
|
|
513
|
+
util.error('Backward pagination (last/before) is not supported on full-list connections; use first/after.', 'UnsupportedPagination');
|
|
514
|
+
}
|
|
509
515
|
const filter = ctx.args.filter ?? {};
|
|
510
516
|
const names = {};
|
|
511
517
|
const values = {};
|
|
@@ -529,10 +535,17 @@ export function request(ctx) {
|
|
|
529
535
|
});
|
|
530
536
|
parts.push('#id IN (' + placeholders.join(', ') + ')');
|
|
531
537
|
}${filterClauses}${rangeClauses}${requireAttributeClause}
|
|
538
|
+
// The cursor is base64(JSON({ token, index })); decode the after arg back to the raw
|
|
539
|
+
// DynamoDB continuation token the response side emitted (Fix 1 round-trip).
|
|
540
|
+
let after = null;
|
|
541
|
+
if (ctx.args.after != null && ctx.args.after !== '') {
|
|
542
|
+
const parsed = JSON.parse(util.base64Decode(ctx.args.after));
|
|
543
|
+
after = parsed.token ?? null;
|
|
544
|
+
}
|
|
532
545
|
const req = {
|
|
533
546
|
operation: 'Scan',
|
|
534
547
|
limit: (ctx.args.first ?? 50),
|
|
535
|
-
nextToken:
|
|
548
|
+
nextToken: after,
|
|
536
549
|
};
|
|
537
550
|
if (parts.length > 0) {
|
|
538
551
|
req.filter = {
|
|
@@ -546,17 +559,27 @@ export function request(ctx) {
|
|
|
546
559
|
export function response(ctx) {
|
|
547
560
|
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
548
561
|
let items = ctx.result?.items ?? [];${sortBlock}
|
|
562
|
+
// One Scan continuation token per page; encode it (with the item's page index for a
|
|
563
|
+
// unique, opaque Relay cursor). The request side decodes .token back to the raw
|
|
564
|
+
// DynamoDB nextToken (Fix 1).
|
|
565
|
+
const next = ctx.result?.nextToken ?? null;
|
|
549
566
|
const edges = items.map((item, i) => ({
|
|
550
567
|
node: item,
|
|
551
|
-
cursor:
|
|
568
|
+
cursor: util.base64Encode(JSON.stringify({ token: next, index: i })),
|
|
552
569
|
}));
|
|
570
|
+
// A filtered/1MB-capped page can be empty or short while next is still set (limit
|
|
571
|
+
// caps rows scanned, not returned). The token is page-level, so synthesise a
|
|
572
|
+
// boundary cursor from it alone so a client can resume past a fully-filtered-out
|
|
573
|
+
// page instead of restarting from page 1 (Fix 3). The request only reads .token,
|
|
574
|
+
// so index -1 is inert on resume.
|
|
575
|
+
const boundary = next ? util.base64Encode(JSON.stringify({ token: next, index: -1 })) : null;
|
|
553
576
|
return {
|
|
554
577
|
edges,
|
|
555
578
|
pageInfo: {
|
|
556
|
-
hasNextPage: !!
|
|
579
|
+
hasNextPage: !!next,
|
|
557
580
|
hasPreviousPage: !!ctx.args.after,
|
|
558
|
-
startCursor: edges.length > 0 ? edges[0].cursor :
|
|
559
|
-
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor :
|
|
581
|
+
startCursor: edges.length > 0 ? edges[0].cursor : boundary,
|
|
582
|
+
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : boundary,
|
|
560
583
|
},
|
|
561
584
|
};
|
|
562
585
|
}
|
|
@@ -408,6 +408,12 @@ function listAllItemsConnection(labelField, filterFieldsOpt, rangeFieldsOpt, sor
|
|
|
408
408
|
}`;
|
|
409
409
|
return importUtil + `
|
|
410
410
|
export function request(ctx) {
|
|
411
|
+
// Scan cannot page backward (ScanIndexForward is Query-only). Fail loud rather than
|
|
412
|
+
// silently returning the forward page. The ordered {single}Items connection
|
|
413
|
+
// (queryItemsWithSortConditions) supports last/before — direct backward callers there.
|
|
414
|
+
if (ctx.args.before != null || ctx.args.last != null) {
|
|
415
|
+
util.error('Backward pagination (last/before) is not supported on full-list connections; use first/after.', 'UnsupportedPagination');
|
|
416
|
+
}
|
|
411
417
|
const filter = ctx.args.filter ?? {};
|
|
412
418
|
const names = {};
|
|
413
419
|
const values = {};
|
|
@@ -431,10 +437,17 @@ export function request(ctx) {
|
|
|
431
437
|
});
|
|
432
438
|
parts.push('#id IN (' + placeholders.join(', ') + ')');
|
|
433
439
|
}` + filterClauses + rangeClauses + requireAttributeClause + `
|
|
440
|
+
// The cursor is base64(JSON({ token, index })); decode the after arg back to the raw
|
|
441
|
+
// DynamoDB continuation token the response side emitted (Fix 1 round-trip).
|
|
442
|
+
let after = null;
|
|
443
|
+
if (ctx.args.after != null && ctx.args.after !== '') {
|
|
444
|
+
const parsed = JSON.parse(util.base64Decode(ctx.args.after));
|
|
445
|
+
after = parsed.token ?? null;
|
|
446
|
+
}
|
|
434
447
|
const req = {
|
|
435
448
|
operation: 'Scan',
|
|
436
449
|
limit: (ctx.args.first ?? 50),
|
|
437
|
-
nextToken:
|
|
450
|
+
nextToken: after,
|
|
438
451
|
};
|
|
439
452
|
if (parts.length > 0) {
|
|
440
453
|
req.filter = {
|
|
@@ -448,17 +461,27 @@ export function request(ctx) {
|
|
|
448
461
|
export function response(ctx) {
|
|
449
462
|
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
450
463
|
let items = ctx.result?.items ?? [];` + sortBlock + `
|
|
464
|
+
// One Scan continuation token per page; encode it (with the item's page index for a
|
|
465
|
+
// unique, opaque Relay cursor). The request side decodes .token back to the raw
|
|
466
|
+
// DynamoDB nextToken (Fix 1).
|
|
467
|
+
const next = ctx.result?.nextToken ?? null;
|
|
451
468
|
const edges = items.map((item, i) => ({
|
|
452
469
|
node: item,
|
|
453
|
-
cursor:
|
|
470
|
+
cursor: util.base64Encode(JSON.stringify({ token: next, index: i })),
|
|
454
471
|
}));
|
|
472
|
+
// A filtered/1MB-capped page can be empty or short while next is still set (limit
|
|
473
|
+
// caps rows scanned, not returned). The token is page-level, so synthesise a
|
|
474
|
+
// boundary cursor from it alone so a client can resume past a fully-filtered-out
|
|
475
|
+
// page instead of restarting from page 1 (Fix 3). The request only reads .token,
|
|
476
|
+
// so index -1 is inert on resume.
|
|
477
|
+
const boundary = next ? util.base64Encode(JSON.stringify({ token: next, index: -1 })) : null;
|
|
455
478
|
return {
|
|
456
479
|
edges,
|
|
457
480
|
pageInfo: {
|
|
458
|
-
hasNextPage: !!
|
|
481
|
+
hasNextPage: !!next,
|
|
459
482
|
hasPreviousPage: !!ctx.args.after,
|
|
460
|
-
startCursor: edges.length > 0 ? edges[0].cursor :
|
|
461
|
-
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor :
|
|
483
|
+
startCursor: edges.length > 0 ? edges[0].cursor : boundary,
|
|
484
|
+
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : boundary,
|
|
462
485
|
},
|
|
463
486
|
};
|
|
464
487
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** @pulumi/aws/location/PlaceIndex
|
|
2
|
+
see: https://www.pulumi.com/registry/packages/aws/api-docs/location/placeindex/
|
|
3
|
+
*/
|
|
4
|
+
type dataSourceConfiguration = {
|
|
5
|
+
// "SingleUse" | "Storage" — geocoding for immediate display uses SingleUse.
|
|
6
|
+
intendedUse?: Pulumi.Input.t<string>,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type args = {
|
|
10
|
+
indexName: Pulumi.Input.t<string>,
|
|
11
|
+
// Provider of geospatial data: "Esri" | "Grab" | "Here".
|
|
12
|
+
dataSource: Pulumi.Input.t<string>,
|
|
13
|
+
dataSourceConfiguration?: Pulumi.Input.t<dataSourceConfiguration>,
|
|
14
|
+
description?: Pulumi.Input.t<string>,
|
|
15
|
+
tags?: Pulumi.Input.t<Aws.tags>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type t = {
|
|
19
|
+
indexName: Pulumi.Output.t<string>,
|
|
20
|
+
indexArn: Pulumi.Output.t<string>,
|
|
21
|
+
createTime: Pulumi.Output.t<string>,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@module("@pulumi/aws") @scope("location") @new
|
|
25
|
+
external make: (~name: string, ~args: args, ~opts: Pulumi.CustomResourceOptions.t=?) => t =
|
|
26
|
+
"PlaceIndex"
|
|
27
|
+
|
|
28
|
+
@module("@pulumi/aws") @scope(("location", "PlaceIndex"))
|
|
29
|
+
external get: (
|
|
30
|
+
~name: string,
|
|
31
|
+
~id: Pulumi.Input.t<string>,
|
|
32
|
+
~opts: Pulumi.CustomResourceOptions.t=?,
|
|
33
|
+
) => t = "get"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { evalResolver, makeCtx } from './resolverTestHelper.mjs'
|
|
2
|
+
import { util } from './__mocks__/appsync-utils.mjs'
|
|
2
3
|
import * as F from '../src/AppSync/AppSync_Resolver_Functions.res.mjs'
|
|
3
4
|
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
@@ -182,7 +183,7 @@ describe('listAllItemsConnection', () => {
|
|
|
182
183
|
expect(result.filter.expressionValues[':id0']).toEqual({ S: 'a' })
|
|
183
184
|
})
|
|
184
185
|
|
|
185
|
-
test('response returns edges + pageInfo
|
|
186
|
+
test('response returns edges + pageInfo carrying the DynamoDB token cursor', () => {
|
|
186
187
|
const ctx = makeCtx({
|
|
187
188
|
args: {},
|
|
188
189
|
result: { items: [{ id: 'a' }, { id: 'b' }], nextToken: 'next' },
|
|
@@ -192,6 +193,60 @@ describe('listAllItemsConnection', () => {
|
|
|
192
193
|
expect(r.edges[0].node).toEqual({ id: 'a' })
|
|
193
194
|
expect(r.pageInfo.hasNextPage).toBe(true)
|
|
194
195
|
expect(r.pageInfo.hasPreviousPage).toBe(false)
|
|
196
|
+
// Fix 1: the cursor encodes the real nextToken, not a positional index.
|
|
197
|
+
expect(JSON.parse(util.base64Decode(r.pageInfo.endCursor)).token).toBe('next')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// Fix 1 (docs/plans/done/aws-scan-connection-cursor-roundtrip.md): the cursor the
|
|
201
|
+
// response emits must decode back to the exact DynamoDB nextToken the request feeds
|
|
202
|
+
// to the next Scan. This is the round-trip that failed before the fix.
|
|
203
|
+
test('endCursor round-trips: response endCursor → request nextToken', () => {
|
|
204
|
+
const page1 = response(makeCtx({
|
|
205
|
+
args: {},
|
|
206
|
+
result: { items: [{ id: 'a' }], nextToken: 'TOK1' },
|
|
207
|
+
}))
|
|
208
|
+
const req2 = request(makeCtx({ args: { after: page1.pageInfo.endCursor } }))
|
|
209
|
+
expect(req2.nextToken).toBe('TOK1')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('final page (nextToken null) closes the connection', () => {
|
|
213
|
+
const r = response(makeCtx({
|
|
214
|
+
args: { after: 'x' },
|
|
215
|
+
result: { items: [{ id: 'z' }], nextToken: null },
|
|
216
|
+
}))
|
|
217
|
+
expect(r.pageInfo.hasNextPage).toBe(false)
|
|
218
|
+
expect(r.pageInfo.hasPreviousPage).toBe(true)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// Fix 3: a filtered Scan can return an empty page while nextToken is still set.
|
|
222
|
+
// The boundary cursor must let the client resume instead of restarting page 1.
|
|
223
|
+
test('empty-but-continuable page yields a resumable boundary cursor', () => {
|
|
224
|
+
const empty = response(makeCtx({
|
|
225
|
+
args: {},
|
|
226
|
+
result: { items: [], nextToken: 'TOK2' },
|
|
227
|
+
}))
|
|
228
|
+
expect(empty.edges).toHaveLength(0)
|
|
229
|
+
expect(empty.pageInfo.hasNextPage).toBe(true)
|
|
230
|
+
expect(empty.pageInfo.endCursor).not.toBeNull()
|
|
231
|
+
const req = request(makeCtx({ args: { after: empty.pageInfo.endCursor } }))
|
|
232
|
+
expect(req.nextToken).toBe('TOK2')
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('empty final page (nextToken null) has a null boundary and stops', () => {
|
|
236
|
+
const r = response(makeCtx({ args: {}, result: { items: [], nextToken: null } }))
|
|
237
|
+
expect(r.pageInfo.endCursor).toBeNull()
|
|
238
|
+
expect(r.pageInfo.hasNextPage).toBe(false)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// Fix 2: Scan cannot page backward; last/before must error, not mislead.
|
|
242
|
+
test('request rejects backward pagination (before)', () => {
|
|
243
|
+
expect(() => request(makeCtx({ args: { before: 'x' } }))).toThrow(
|
|
244
|
+
'Backward pagination',
|
|
245
|
+
)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('request rejects backward pagination (last)', () => {
|
|
249
|
+
expect(() => request(makeCtx({ args: { last: 5 } }))).toThrow('Backward pagination')
|
|
195
250
|
})
|
|
196
251
|
})
|
|
197
252
|
|
|
@@ -12,6 +12,8 @@ export const util = {
|
|
|
12
12
|
toStringSet: arr => ({ SS: arr }),
|
|
13
13
|
toNumberSet: arr => ({ NS: arr.map(String) }),
|
|
14
14
|
},
|
|
15
|
+
base64Encode: str => Buffer.from(String(str), 'utf8').toString('base64'),
|
|
16
|
+
base64Decode: b64 => Buffer.from(String(b64), 'base64').toString('utf8'),
|
|
15
17
|
error: (msg, type) => { throw Object.assign(new Error(msg), { errorType: type }) },
|
|
16
18
|
unauthorized: () => { throw new Error('Unauthorized') },
|
|
17
19
|
defaultIfNull: (val, def) => (val === null || val === undefined ? def : val),
|