@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/rescript-pulumi-aws",
3
- "version": "2.4.0-alpha.58",
3
+ "version": "2.4.0-alpha.60",
4
4
  "description": "ReScript bindings for @pulumi/aws",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -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: (ctx.args.after ?? null),
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: ctx.args.after ? ctx.args.after + '_' + i : '' + i,
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: !!ctx.result?.nextToken,
579
+ hasNextPage: !!next,
557
580
  hasPreviousPage: !!ctx.args.after,
558
- startCursor: edges.length > 0 ? edges[0].cursor : null,
559
- endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null,
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: (ctx.args.after ?? null),
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: ctx.args.after ? ctx.args.after + '_' + i : '' + i,
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: !!ctx.result?.nextToken,
481
+ hasNextPage: !!next,
459
482
  hasPreviousPage: !!ctx.args.after,
460
- startCursor: edges.length > 0 ? edges[0].cursor : null,
461
- endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null,
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,4 @@
1
+ /** @pulumi/aws/location
2
+ see: https://www.pulumi.com/registry/packages/aws/api-docs/location/
3
+ */
4
+ module PlaceIndex = Location_PlaceIndex
@@ -0,0 +1,9 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ let PlaceIndex;
5
+
6
+ export {
7
+ PlaceIndex,
8
+ }
9
+ /* No side effect */
@@ -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"
@@ -0,0 +1,2 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+ /* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */
@@ -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 with positional cursors', () => {
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),