@model-ts/dynamodb 1.0.0 → 1.2.0

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/__test__/client-with-cursor-encryption.test.d.ts +1 -0
  3. package/dist/cjs/__test__/client-with-cursor-encryption.test.js +1911 -0
  4. package/dist/cjs/__test__/client-with-cursor-encryption.test.js.map +1 -0
  5. package/dist/cjs/__test__/client.test.js +77 -56
  6. package/dist/cjs/__test__/client.test.js.map +1 -1
  7. package/dist/cjs/client.d.ts +14 -2
  8. package/dist/cjs/client.js +28 -9
  9. package/dist/cjs/client.js.map +1 -1
  10. package/dist/cjs/dynamodb-model.d.ts +4 -0
  11. package/dist/cjs/pagination.d.ts +48 -3
  12. package/dist/cjs/pagination.js +65 -23
  13. package/dist/cjs/pagination.js.map +1 -1
  14. package/dist/cjs/provider.d.ts +21 -20
  15. package/dist/cjs/provider.js +5 -1
  16. package/dist/cjs/provider.js.map +1 -1
  17. package/dist/esm/__test__/client-with-cursor-encryption.test.d.ts +1 -0
  18. package/dist/esm/__test__/client-with-cursor-encryption.test.js +1909 -0
  19. package/dist/esm/__test__/client-with-cursor-encryption.test.js.map +1 -0
  20. package/dist/esm/__test__/client.test.js +78 -57
  21. package/dist/esm/__test__/client.test.js.map +1 -1
  22. package/dist/esm/client.d.ts +14 -2
  23. package/dist/esm/client.js +28 -9
  24. package/dist/esm/client.js.map +1 -1
  25. package/dist/esm/dynamodb-model.d.ts +4 -0
  26. package/dist/esm/pagination.d.ts +48 -3
  27. package/dist/esm/pagination.js +64 -23
  28. package/dist/esm/pagination.js.map +1 -1
  29. package/dist/esm/provider.d.ts +21 -20
  30. package/dist/esm/provider.js +5 -1
  31. package/dist/esm/provider.js.map +1 -1
  32. package/package.json +2 -2
  33. package/src/__test__/client-with-cursor-encryption.test.ts +2445 -0
  34. package/src/__test__/client.test.ts +92 -57
  35. package/src/client.ts +54 -13
  36. package/src/dynamodb-model.ts +4 -0
  37. package/src/pagination.ts +76 -19
  38. package/src/provider.ts +5 -2
@@ -0,0 +1,2445 @@
1
+ import * as t from "io-ts"
2
+ import { model, RuntimeTypeValidationError, union } from "@model-ts/core"
3
+ import { Sandbox, createSandbox } from "../sandbox"
4
+ import { Client } from "../client"
5
+ import { getProvider } from "../provider"
6
+ import {
7
+ KeyExistsError,
8
+ ItemNotFoundError,
9
+ ConditionalCheckFailedError,
10
+ RaceConditionError,
11
+ BulkWriteTransactionError
12
+ } from "../errors"
13
+
14
+ const client = new Client({
15
+ tableName: "table",
16
+ cursorEncryptionKey: Buffer.from(
17
+ "0tpsnnd7+k7xD5pMxK9TXAEkB6c/GYkkW3HEy7ZKBOs=",
18
+ "base64"
19
+ )
20
+ })
21
+ const provider = getProvider(client)
22
+
23
+ const SIMPLE_CODEC = t.type({
24
+ foo: t.string,
25
+ bar: t.number
26
+ })
27
+
28
+ class Simple extends model("Simple", SIMPLE_CODEC, provider) {
29
+ get PK() {
30
+ return `PK#${this.foo}`
31
+ }
32
+
33
+ get SK() {
34
+ return `SK#${this.bar}`
35
+ }
36
+ }
37
+
38
+ class SingleGSI extends model("SingleGSI", SIMPLE_CODEC, provider) {
39
+ get PK() {
40
+ return `PK#${this.foo}`
41
+ }
42
+ get SK() {
43
+ return `SK#${this.bar}`
44
+ }
45
+ get GSI2PK() {
46
+ return `GSI2PK#${this.foo}${this.foo}`
47
+ }
48
+ get GSI2SK() {
49
+ return `GSI2SK#FIXED`
50
+ }
51
+ }
52
+
53
+ class MultiGSI extends model("MultiGSI", SIMPLE_CODEC, provider) {
54
+ get PK() {
55
+ return `PK#${this.foo}`
56
+ }
57
+ get SK() {
58
+ return `SK#${this.bar}`
59
+ }
60
+ get GSI2PK() {
61
+ return `GSI2PK#${this.foo}${this.foo}`
62
+ }
63
+ get GSI2SK() {
64
+ return `GSI2SK#FIXED`
65
+ }
66
+ get GSI3PK() {
67
+ return `GSI3PK#FIXED`
68
+ }
69
+ get GSI3SK() {
70
+ return `GSI3SK#${this.bar}${this.bar}`
71
+ }
72
+ get GSI4PK() {
73
+ return `GSI4PK#FIXED`
74
+ }
75
+ get GSI4SK() {
76
+ return `GSI4SK#${this.bar}${this.bar}`
77
+ }
78
+ get GSI5PK() {
79
+ return `GSI5PK#FIXED`
80
+ }
81
+ get GSI5SK() {
82
+ return `GSI5SK#${this.bar}${this.bar}`
83
+ }
84
+ }
85
+
86
+ class A extends model(
87
+ "A",
88
+ t.type({ pk: t.string, sk: t.string, a: t.number }),
89
+ provider
90
+ ) {
91
+ get PK() {
92
+ return this.pk
93
+ }
94
+ get SK() {
95
+ return this.sk
96
+ }
97
+ }
98
+ class B extends model(
99
+ "B",
100
+ t.type({ pk: t.string, sk: t.string, b: t.string }),
101
+ provider
102
+ ) {
103
+ get PK() {
104
+ return this.pk
105
+ }
106
+ get SK() {
107
+ return this.sk
108
+ }
109
+ }
110
+ class C extends model(
111
+ "C",
112
+ t.type({ pk: t.string, sk: t.string, c: t.string }),
113
+ provider
114
+ ) {
115
+ get PK() {
116
+ return this.pk
117
+ }
118
+ get SK() {
119
+ return this.sk
120
+ }
121
+ }
122
+
123
+ class D extends model(
124
+ "D",
125
+ t.type({ pk: t.string, sk: t.string, d: t.string }),
126
+ provider
127
+ ) {
128
+ get PK() {
129
+ return this.pk
130
+ }
131
+ get SK() {
132
+ return this.sk
133
+ }
134
+ }
135
+
136
+ class Union extends union([C, D], provider) {}
137
+
138
+ let sandbox: Sandbox
139
+ beforeEach(async () => {
140
+ sandbox = await createSandbox(client)
141
+ })
142
+
143
+ describe("put", () => {
144
+ describe("via instance", () => {
145
+ test("it inserts a simple model", async () => {
146
+ const before = await sandbox.snapshot()
147
+
148
+ await new Simple({ foo: "hi", bar: 42 }).put()
149
+
150
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
151
+ Snapshot Diff:
152
+ - First value
153
+ + Second value
154
+
155
+ - Object {}
156
+ + Object {
157
+ + "PK#hi__SK#42": Object {
158
+ + "PK": "PK#hi",
159
+ + "SK": "SK#42",
160
+ + "_docVersion": 0,
161
+ + "_tag": "Simple",
162
+ + "bar": 42,
163
+ + "foo": "hi",
164
+ + },
165
+ + }
166
+ `)
167
+ })
168
+
169
+ test("it inserts a model with single gsi", async () => {
170
+ const before = await sandbox.snapshot()
171
+
172
+ await new SingleGSI({ foo: "yes", bar: 42 }).put()
173
+
174
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
175
+ Snapshot Diff:
176
+ - First value
177
+ + Second value
178
+
179
+ - Object {}
180
+ + Object {
181
+ + "PK#yes__SK#42": Object {
182
+ + "GSI2PK": "GSI2PK#yesyes",
183
+ + "GSI2SK": "GSI2SK#FIXED",
184
+ + "PK": "PK#yes",
185
+ + "SK": "SK#42",
186
+ + "_docVersion": 0,
187
+ + "_tag": "SingleGSI",
188
+ + "bar": 42,
189
+ + "foo": "yes",
190
+ + },
191
+ + }
192
+ `)
193
+ })
194
+
195
+ test("it inserts a model with multiple gsi", async () => {
196
+ const before = await sandbox.snapshot()
197
+
198
+ await new MultiGSI({ foo: "yes", bar: 42 }).put()
199
+
200
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
201
+ Snapshot Diff:
202
+ - First value
203
+ + Second value
204
+
205
+ - Object {}
206
+ + Object {
207
+ + "PK#yes__SK#42": Object {
208
+ + "GSI2PK": "GSI2PK#yesyes",
209
+ + "GSI2SK": "GSI2SK#FIXED",
210
+ + "GSI3PK": "GSI3PK#FIXED",
211
+ + "GSI3SK": "GSI3SK#4242",
212
+ + "GSI4PK": "GSI4PK#FIXED",
213
+ + "GSI4SK": "GSI4SK#4242",
214
+ + "GSI5PK": "GSI5PK#FIXED",
215
+ + "GSI5SK": "GSI5SK#4242",
216
+ + "PK": "PK#yes",
217
+ + "SK": "SK#42",
218
+ + "_docVersion": 0,
219
+ + "_tag": "MultiGSI",
220
+ + "bar": 42,
221
+ + "foo": "yes",
222
+ + },
223
+ + }
224
+ `)
225
+ })
226
+
227
+ test("it throws KeyExistsError if item exists", async () => {
228
+ await new MultiGSI({ foo: "yes", bar: 42 }).put()
229
+
230
+ await expect(
231
+ new MultiGSI({ foo: "yes", bar: 42 }).put()
232
+ ).rejects.toBeInstanceOf(KeyExistsError)
233
+ })
234
+
235
+ test("it overwrites item if `ignoreExistence` is set", async () => {
236
+ await new MultiGSI({ foo: "yes", bar: 42 }).put()
237
+
238
+ await expect(
239
+ new MultiGSI({ foo: "yes", bar: 42 }).put({ IgnoreExistence: true })
240
+ ).resolves.toBeInstanceOf(MultiGSI)
241
+ })
242
+ })
243
+
244
+ describe("via model", () => {
245
+ test("it inserts a simple model", async () => {
246
+ const before = await sandbox.snapshot()
247
+
248
+ await Simple.put(new Simple({ foo: "hi", bar: 42 }))
249
+
250
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
251
+ Snapshot Diff:
252
+ - First value
253
+ + Second value
254
+
255
+ - Object {}
256
+ + Object {
257
+ + "PK#hi__SK#42": Object {
258
+ + "PK": "PK#hi",
259
+ + "SK": "SK#42",
260
+ + "_docVersion": 0,
261
+ + "_tag": "Simple",
262
+ + "bar": 42,
263
+ + "foo": "hi",
264
+ + },
265
+ + }
266
+ `)
267
+ })
268
+
269
+ test("it inserts a model with single gsi", async () => {
270
+ const before = await sandbox.snapshot()
271
+
272
+ await SingleGSI.put(new SingleGSI({ foo: "yes", bar: 42 }))
273
+
274
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
275
+ Snapshot Diff:
276
+ - First value
277
+ + Second value
278
+
279
+ - Object {}
280
+ + Object {
281
+ + "PK#yes__SK#42": Object {
282
+ + "GSI2PK": "GSI2PK#yesyes",
283
+ + "GSI2SK": "GSI2SK#FIXED",
284
+ + "PK": "PK#yes",
285
+ + "SK": "SK#42",
286
+ + "_docVersion": 0,
287
+ + "_tag": "SingleGSI",
288
+ + "bar": 42,
289
+ + "foo": "yes",
290
+ + },
291
+ + }
292
+ `)
293
+ })
294
+
295
+ test("it inserts a model with multiple gsi", async () => {
296
+ const before = await sandbox.snapshot()
297
+
298
+ await MultiGSI.put(new MultiGSI({ foo: "yes", bar: 42 }))
299
+
300
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
301
+ Snapshot Diff:
302
+ - First value
303
+ + Second value
304
+
305
+ - Object {}
306
+ + Object {
307
+ + "PK#yes__SK#42": Object {
308
+ + "GSI2PK": "GSI2PK#yesyes",
309
+ + "GSI2SK": "GSI2SK#FIXED",
310
+ + "GSI3PK": "GSI3PK#FIXED",
311
+ + "GSI3SK": "GSI3SK#4242",
312
+ + "GSI4PK": "GSI4PK#FIXED",
313
+ + "GSI4SK": "GSI4SK#4242",
314
+ + "GSI5PK": "GSI5PK#FIXED",
315
+ + "GSI5SK": "GSI5SK#4242",
316
+ + "PK": "PK#yes",
317
+ + "SK": "SK#42",
318
+ + "_docVersion": 0,
319
+ + "_tag": "MultiGSI",
320
+ + "bar": 42,
321
+ + "foo": "yes",
322
+ + },
323
+ + }
324
+ `)
325
+ })
326
+
327
+ test("it throws KeyExistsError if item exists", async () => {
328
+ await MultiGSI.put(new MultiGSI({ foo: "yes", bar: 42 }))
329
+
330
+ await expect(
331
+ new MultiGSI({ foo: "yes", bar: 42 }).put()
332
+ ).rejects.toBeInstanceOf(KeyExistsError)
333
+ })
334
+
335
+ test("it overwrites item if `ignoreExistence` is set", async () => {
336
+ await MultiGSI.put(new MultiGSI({ foo: "yes", bar: 42 }))
337
+
338
+ await expect(
339
+ MultiGSI.put(new MultiGSI({ foo: "yes", bar: 42 }), {
340
+ IgnoreExistence: true
341
+ })
342
+ ).resolves.toBeInstanceOf(MultiGSI)
343
+ })
344
+ })
345
+ })
346
+
347
+ describe("get", () => {
348
+ describe("via model", () => {
349
+ test("it throws `ItemNotFoundError` if item doesn't exist", async () => {
350
+ await expect(
351
+ Simple.get({ PK: "any", SK: "thing" })
352
+ ).rejects.toBeInstanceOf(ItemNotFoundError)
353
+ })
354
+
355
+ test("it returns the item", async () => {
356
+ const item = await new Simple({ foo: "hi", bar: 432 }).put()
357
+
358
+ const result = await Simple.get({
359
+ PK: item.keys().PK,
360
+ SK: item.keys().SK
361
+ })
362
+
363
+ expect(result.values()).toMatchInlineSnapshot(`
364
+ Object {
365
+ "bar": 432,
366
+ "foo": "hi",
367
+ }
368
+ `)
369
+
370
+ expect(result.encode()).toEqual(item.encode())
371
+ })
372
+
373
+ test("it throws `RuntimeTypeError` if item can't be decoded", async () => {
374
+ await sandbox.seed({ PK: "A", SK: "A", c: 324 })
375
+
376
+ await expect(Simple.get({ PK: "A", SK: "A" })).rejects.toBeInstanceOf(
377
+ RuntimeTypeValidationError
378
+ )
379
+ })
380
+ })
381
+
382
+ describe("via union", () => {
383
+ test("it throws `ItemNotFoundError` if item doesn't exist", async () => {
384
+ await expect(
385
+ Union.get({ PK: "any", SK: "thing" })
386
+ ).rejects.toBeInstanceOf(ItemNotFoundError)
387
+ })
388
+
389
+ test("it returns the item", async () => {
390
+ const item = await new C({ pk: "PK#0", sk: "SK#0", c: "0" }).put()
391
+
392
+ const result = await Union.get(item.keys())
393
+
394
+ expect(result).toBeInstanceOf(C)
395
+ expect(result.values()).toMatchInlineSnapshot(`
396
+ Object {
397
+ "c": "0",
398
+ "pk": "PK#0",
399
+ "sk": "SK#0",
400
+ }
401
+ `)
402
+ })
403
+
404
+ test("it throws `RuntimeTypeError` if item can't be decoded", async () => {
405
+ await sandbox.seed({ PK: "A", SK: "A", a: 324 })
406
+
407
+ await expect(Union.get({ PK: "A", SK: "A" })).rejects.toBeInstanceOf(
408
+ RuntimeTypeValidationError
409
+ )
410
+ })
411
+ })
412
+ })
413
+
414
+ describe("delete", () => {
415
+ describe("via client", () => {
416
+ test("it deletes the item and returns null", async () => {
417
+ const item = await new Simple({ foo: "hi", bar: 432 }).put()
418
+
419
+ const before = await sandbox.snapshot()
420
+
421
+ const result = await client.delete({
422
+ _operation: "delete",
423
+ _model: Simple,
424
+ key: {
425
+ PK: item.keys().PK,
426
+ SK: item.keys().SK
427
+ }
428
+ })
429
+
430
+ expect(result).toBeNull()
431
+
432
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
433
+ Snapshot Diff:
434
+ - First value
435
+ + Second value
436
+
437
+ - Object {
438
+ - "PK#hi__SK#432": Object {
439
+ - "PK": "PK#hi",
440
+ - "SK": "SK#432",
441
+ - "_docVersion": 0,
442
+ - "_tag": "Simple",
443
+ - "bar": 432,
444
+ - "foo": "hi",
445
+ - },
446
+ - }
447
+ + Object {}
448
+ `)
449
+ })
450
+ })
451
+
452
+ describe("via model", () => {
453
+ test("it deletes the item and returns null", async () => {
454
+ const item = await new Simple({ foo: "hi", bar: 432 }).put()
455
+
456
+ const before = await sandbox.snapshot()
457
+
458
+ const result = await Simple.delete({
459
+ PK: item.keys().PK,
460
+ SK: item.keys().SK
461
+ })
462
+
463
+ expect(result).toBeNull()
464
+
465
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
466
+ Snapshot Diff:
467
+ - First value
468
+ + Second value
469
+
470
+ - Object {
471
+ - "PK#hi__SK#432": Object {
472
+ - "PK": "PK#hi",
473
+ - "SK": "SK#432",
474
+ - "_docVersion": 0,
475
+ - "_tag": "Simple",
476
+ - "bar": 432,
477
+ - "foo": "hi",
478
+ - },
479
+ - }
480
+ + Object {}
481
+ `)
482
+ })
483
+ })
484
+
485
+ describe("via instance", () => {
486
+ test("it deletes the item and returns null", async () => {
487
+ const item = await new Simple({ foo: "hi", bar: 432 }).put()
488
+
489
+ const before = await sandbox.snapshot()
490
+
491
+ const result = await item.delete()
492
+
493
+ expect(result).toBeNull()
494
+
495
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
496
+ Snapshot Diff:
497
+ - First value
498
+ + Second value
499
+
500
+ - Object {
501
+ - "PK#hi__SK#432": Object {
502
+ - "PK": "PK#hi",
503
+ - "SK": "SK#432",
504
+ - "_docVersion": 0,
505
+ - "_tag": "Simple",
506
+ - "bar": 432,
507
+ - "foo": "hi",
508
+ - },
509
+ - }
510
+ + Object {}
511
+ `)
512
+ })
513
+ })
514
+ })
515
+
516
+ describe("softDelete", () => {
517
+ describe("via client", () => {
518
+ test("it soft-deletes the item", async () => {
519
+ const item = await new Simple({ foo: "hi", bar: 432 }).put()
520
+ const withGSI = await new MultiGSI({ foo: "hello", bar: 42 }).put()
521
+
522
+ const before = await sandbox.snapshot()
523
+
524
+ const simpleResult = await client.softDelete(item)
525
+ const withGSIResult = await client.softDelete(withGSI)
526
+
527
+ expect(simpleResult.values()).toMatchInlineSnapshot(`
528
+ Object {
529
+ "bar": 432,
530
+ "foo": "hi",
531
+ }
532
+ `)
533
+ expect(withGSIResult.values()).toMatchInlineSnapshot(`
534
+ Object {
535
+ "bar": 42,
536
+ "foo": "hello",
537
+ }
538
+ `)
539
+
540
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
541
+ Snapshot Diff:
542
+ - First value
543
+ + Second value
544
+
545
+ @@ -1,25 +1,27 @@
546
+ Object {
547
+ - "PK#hello__SK#42": Object {
548
+ - "GSI2PK": "GSI2PK#hellohello",
549
+ - "GSI2SK": "GSI2SK#FIXED",
550
+ - "GSI3PK": "GSI3PK#FIXED",
551
+ - "GSI3SK": "GSI3SK#4242",
552
+ - "GSI4PK": "GSI4PK#FIXED",
553
+ - "GSI4SK": "GSI4SK#4242",
554
+ - "GSI5PK": "GSI5PK#FIXED",
555
+ - "GSI5SK": "GSI5SK#4242",
556
+ - "PK": "PK#hello",
557
+ - "SK": "SK#42",
558
+ + "$$DELETED$$PK#hello__$$DELETED$$SK#42": Object {
559
+ + "GSI2PK": "$$DELETED$$GSI2PK#hellohello",
560
+ + "GSI2SK": "$$DELETED$$GSI2SK#FIXED",
561
+ + "GSI3PK": "$$DELETED$$GSI3PK#FIXED",
562
+ + "GSI3SK": "$$DELETED$$GSI3SK#4242",
563
+ + "GSI4PK": "$$DELETED$$GSI4PK#FIXED",
564
+ + "GSI4SK": "$$DELETED$$GSI4SK#4242",
565
+ + "GSI5PK": "$$DELETED$$GSI5PK#FIXED",
566
+ + "GSI5SK": "$$DELETED$$GSI5SK#4242",
567
+ + "PK": "$$DELETED$$PK#hello",
568
+ + "SK": "$$DELETED$$SK#42",
569
+ + "_deletedAt": "2021-05-01T08:00:00.000Z",
570
+ "_docVersion": 0,
571
+ "_tag": "MultiGSI",
572
+ "bar": 42,
573
+ "foo": "hello",
574
+ },
575
+ - "PK#hi__SK#432": Object {
576
+ - "PK": "PK#hi",
577
+ - "SK": "SK#432",
578
+ + "$$DELETED$$PK#hi__$$DELETED$$SK#432": Object {
579
+ + "PK": "$$DELETED$$PK#hi",
580
+ + "SK": "$$DELETED$$SK#432",
581
+ + "_deletedAt": "2021-05-01T08:00:00.000Z",
582
+ "_docVersion": 0,
583
+ "_tag": "Simple",
584
+ "bar": 432,
585
+ "foo": "hi",
586
+ },
587
+ `)
588
+ })
589
+ })
590
+
591
+ describe("via model", () => {
592
+ test("it soft-deletes the item", async () => {
593
+ const item = await new Simple({ foo: "hi", bar: 432 }).put()
594
+
595
+ const before = await sandbox.snapshot()
596
+
597
+ const result = await Simple.softDelete(item)
598
+
599
+ expect(result.values()).toMatchInlineSnapshot(`
600
+ Object {
601
+ "bar": 432,
602
+ "foo": "hi",
603
+ }
604
+ `)
605
+
606
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
607
+ Snapshot Diff:
608
+ - First value
609
+ + Second value
610
+
611
+ @@ -1,9 +1,10 @@
612
+ Object {
613
+ - "PK#hi__SK#432": Object {
614
+ - "PK": "PK#hi",
615
+ - "SK": "SK#432",
616
+ + "$$DELETED$$PK#hi__$$DELETED$$SK#432": Object {
617
+ + "PK": "$$DELETED$$PK#hi",
618
+ + "SK": "$$DELETED$$SK#432",
619
+ + "_deletedAt": "2021-05-01T08:00:00.000Z",
620
+ "_docVersion": 0,
621
+ "_tag": "Simple",
622
+ "bar": 432,
623
+ "foo": "hi",
624
+ },
625
+ `)
626
+ })
627
+ })
628
+
629
+ describe("via instance", () => {
630
+ test("it soft-deletes the item", async () => {
631
+ const item = await new Simple({ foo: "hi", bar: 432 }).put()
632
+
633
+ const before = await sandbox.snapshot()
634
+
635
+ const result = await item.softDelete()
636
+
637
+ expect(result.values()).toMatchInlineSnapshot(`
638
+ Object {
639
+ "bar": 432,
640
+ "foo": "hi",
641
+ }
642
+ `)
643
+
644
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
645
+ Snapshot Diff:
646
+ - First value
647
+ + Second value
648
+
649
+ @@ -1,9 +1,10 @@
650
+ Object {
651
+ - "PK#hi__SK#432": Object {
652
+ - "PK": "PK#hi",
653
+ - "SK": "SK#432",
654
+ + "$$DELETED$$PK#hi__$$DELETED$$SK#432": Object {
655
+ + "PK": "$$DELETED$$PK#hi",
656
+ + "SK": "$$DELETED$$SK#432",
657
+ + "_deletedAt": "2021-05-01T08:00:00.000Z",
658
+ "_docVersion": 0,
659
+ "_tag": "Simple",
660
+ "bar": 432,
661
+ "foo": "hi",
662
+ },
663
+ `)
664
+ })
665
+ })
666
+ })
667
+
668
+ describe("updateRaw", () => {
669
+ test("it throws `ItemNotFoundError` if item doesn't exist", async () => {
670
+ await expect(
671
+ Simple.updateRaw({ PK: "not", SK: "existent" }, { foo: "new foo" })
672
+ ).rejects.toBeInstanceOf(ItemNotFoundError)
673
+ })
674
+
675
+ test("it throws `ConditionalCheckFailedError` if custom condition expression fails", async () => {
676
+ await expect(
677
+ Simple.updateRaw(
678
+ { PK: "not", SK: "existent" },
679
+ { foo: "new foo" },
680
+ { ConditionExpression: "PK = somethingelse" }
681
+ )
682
+ ).rejects.toBeInstanceOf(ConditionalCheckFailedError)
683
+ })
684
+
685
+ test("IT DOES NOT UPDATE KEYS AUTOMATICALLY", async () => {
686
+ const item = await new Simple({ foo: "old", bar: 43 }).put()
687
+
688
+ const result = await Simple.updateRaw(
689
+ { PK: item.PK, SK: item.SK },
690
+ { foo: "new foo" }
691
+ )
692
+
693
+ // NOTE: although the result of updateRaw seems to hold the correct keys, it's important to note
694
+ // that it is not reflected in the DB!
695
+ expect(result.PK).toEqual(`PK#new foo`)
696
+ expect(await sandbox.snapshot()).toMatchInlineSnapshot(`
697
+ Object {
698
+ "PK#old__SK#43": Object {
699
+ "PK": "PK#old",
700
+ "SK": "SK#43",
701
+ "_docVersion": 0,
702
+ "_tag": "Simple",
703
+ "bar": 43,
704
+ "foo": "new foo",
705
+ },
706
+ }
707
+ `)
708
+ })
709
+ })
710
+
711
+ describe("update", () => {
712
+ describe("in-place", () => {
713
+ class InPlace extends model(
714
+ "InPlace",
715
+ t.type({ foo: t.string, bar: t.number }),
716
+ provider
717
+ ) {
718
+ get PK() {
719
+ return "FIXEDPK"
720
+ }
721
+
722
+ get SK() {
723
+ return "FIXEDSK"
724
+ }
725
+ }
726
+
727
+ test("it puts the item if it wasn't stored before", async () => {
728
+ const item = new InPlace({ foo: "hello", bar: 1 })
729
+
730
+ await item.update({ foo: "ciao" })
731
+
732
+ expect(await sandbox.snapshot()).toMatchInlineSnapshot(`
733
+ Object {
734
+ "FIXEDPK__FIXEDSK": Object {
735
+ "PK": "FIXEDPK",
736
+ "SK": "FIXEDSK",
737
+ "_docVersion": 1,
738
+ "_tag": "InPlace",
739
+ "bar": 1,
740
+ "foo": "ciao",
741
+ },
742
+ }
743
+ `)
744
+ })
745
+
746
+ test("it throws `RaceConditionError` if item was manipulated inbetween", async () => {
747
+ const item = await new InPlace({ foo: "hello", bar: 1 }).put()
748
+ await item.update({ foo: "ciao" })
749
+
750
+ await expect(item.update({ foo: "good luck" })).rejects.toBeInstanceOf(
751
+ RaceConditionError
752
+ )
753
+ })
754
+
755
+ test("it updates an item in-place", async () => {
756
+ const item = await new InPlace({ foo: "hello", bar: 1 }).put()
757
+
758
+ const before = await sandbox.snapshot()
759
+
760
+ expect((await item.update({ foo: "ciao" })).values())
761
+ .toMatchInlineSnapshot(`
762
+ Object {
763
+ "bar": 1,
764
+ "foo": "ciao",
765
+ }
766
+ `)
767
+
768
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
769
+ Snapshot Diff:
770
+ - First value
771
+ + Second value
772
+
773
+ Object {
774
+ "FIXEDPK__FIXEDSK": Object {
775
+ "PK": "FIXEDPK",
776
+ "SK": "FIXEDSK",
777
+ - "_docVersion": 0,
778
+ + "_docVersion": 1,
779
+ "_tag": "InPlace",
780
+ "bar": 1,
781
+ - "foo": "hello",
782
+ + "foo": "ciao",
783
+ },
784
+ }
785
+ `)
786
+ })
787
+ })
788
+ })
789
+
790
+ describe("applyUpdate", () => {
791
+ test("it returns the updated item and update operation", async () => {
792
+ const item = await new A({ pk: "PK", sk: "SK", a: 1 }).put()
793
+
794
+ const before = await sandbox.snapshot()
795
+
796
+ const [updatedItem, updateOp] = item.applyUpdate({ a: 2 })
797
+ expect(updatedItem.values()).toMatchInlineSnapshot(`
798
+ Object {
799
+ "a": 2,
800
+ "pk": "PK",
801
+ "sk": "SK",
802
+ }
803
+ `)
804
+
805
+ await client.bulk([updateOp])
806
+
807
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
808
+ Snapshot Diff:
809
+ - First value
810
+ + Second value
811
+
812
+ Object {
813
+ "PK__SK": Object {
814
+ "PK": "PK",
815
+ "SK": "SK",
816
+ - "_docVersion": 0,
817
+ + "_docVersion": 1,
818
+ "_tag": "A",
819
+ - "a": 1,
820
+ + "a": 2,
821
+ "pk": "PK",
822
+ "sk": "SK",
823
+ },
824
+ }
825
+ `)
826
+ })
827
+ })
828
+
829
+ describe("query", () => {
830
+ test("it returns empty results", async () => {
831
+ expect(
832
+ await client.query(
833
+ {
834
+ KeyConditionExpression: `PK = :pk and begins_with(SK, :sk)`,
835
+ ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT" }
836
+ },
837
+ { a: A, b: B, union: Union }
838
+ )
839
+ ).toMatchInlineSnapshot(`
840
+ Object {
841
+ "_unknown": Array [],
842
+ "a": Array [],
843
+ "b": Array [],
844
+ "meta": Object {
845
+ "lastEvaluatedKey": undefined,
846
+ },
847
+ "union": Array [],
848
+ }
849
+ `)
850
+ })
851
+
852
+ test("it returns unknown results", async () => {
853
+ await sandbox.seed({ PK: "abc", SK: "SORT#1", doesnt: "match" })
854
+
855
+ expect(
856
+ await client.query(
857
+ {
858
+ KeyConditionExpression: `PK = :pk and begins_with(SK, :sk)`,
859
+ ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT#" }
860
+ },
861
+ { a: A, b: B, union: Union }
862
+ )
863
+ ).toMatchInlineSnapshot(`
864
+ Object {
865
+ "_unknown": Array [
866
+ Object {
867
+ "PK": "abc",
868
+ "SK": "SORT#1",
869
+ "doesnt": "match",
870
+ },
871
+ ],
872
+ "a": Array [],
873
+ "b": Array [],
874
+ "meta": Object {
875
+ "lastEvaluatedKey": undefined,
876
+ },
877
+ "union": Array [],
878
+ }
879
+ `)
880
+ })
881
+
882
+ test("it returns results", async () => {
883
+ await sandbox.seed(
884
+ new A({ pk: "abc", sk: "SORT#1", a: 1 }),
885
+ new A({ pk: "abc", sk: "SORT#2", a: 2 }),
886
+ new B({ pk: "abc", sk: "SORT#3", b: "hi" }),
887
+ { PK: "abc", SK: "SORT#4", probably: "unknown" },
888
+ new C({ pk: "abc", sk: "SORT#5", c: "hi" }),
889
+ new D({ pk: "abc", sk: "SORT#6", d: "hi" })
890
+ )
891
+
892
+ const { a, b, union, _unknown, meta } = await client.query(
893
+ {
894
+ KeyConditionExpression: `PK = :pk and begins_with(SK, :sk)`,
895
+ ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT#" }
896
+ },
897
+ { a: A, b: B, union: Union }
898
+ )
899
+
900
+ expect({
901
+ meta: meta,
902
+ _unknown: _unknown,
903
+ a: a.map(item => item.values()),
904
+ b: b.map(item => item.values()),
905
+ union: union.map(item => item.values())
906
+ }).toMatchInlineSnapshot(`
907
+ Object {
908
+ "_unknown": Array [
909
+ Object {
910
+ "PK": "abc",
911
+ "SK": "SORT#4",
912
+ "probably": "unknown",
913
+ },
914
+ ],
915
+ "a": Array [
916
+ Object {
917
+ "a": 1,
918
+ "pk": "abc",
919
+ "sk": "SORT#1",
920
+ },
921
+ Object {
922
+ "a": 2,
923
+ "pk": "abc",
924
+ "sk": "SORT#2",
925
+ },
926
+ ],
927
+ "b": Array [
928
+ Object {
929
+ "b": "hi",
930
+ "pk": "abc",
931
+ "sk": "SORT#3",
932
+ },
933
+ ],
934
+ "meta": Object {
935
+ "lastEvaluatedKey": undefined,
936
+ },
937
+ "union": Array [
938
+ Object {
939
+ "c": "hi",
940
+ "pk": "abc",
941
+ "sk": "SORT#5",
942
+ },
943
+ Object {
944
+ "d": "hi",
945
+ "pk": "abc",
946
+ "sk": "SORT#6",
947
+ },
948
+ ],
949
+ }
950
+ `)
951
+ })
952
+
953
+ test("it paginates", async () => {
954
+ await sandbox.seed(
955
+ ...Array.from({ length: 20 }).map(
956
+ (_, i) =>
957
+ new A({ pk: "abc", sk: `SORT#${String(i).padStart(2, "0")}`, a: i })
958
+ ),
959
+ ...Array.from({ length: 20 }).map(
960
+ (_, i) => new B({ pk: "abc", sk: `SORT#${i + 20}`, b: "bar" })
961
+ )
962
+ )
963
+
964
+ const firstPage = await client.query(
965
+ {
966
+ KeyConditionExpression: `PK = :pk and begins_with(SK, :sk)`,
967
+ ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT#" },
968
+ Limit: 30
969
+ },
970
+ { a: A, b: B }
971
+ )
972
+
973
+ expect(firstPage.a.length).toBe(20)
974
+ expect(firstPage.b.length).toBe(10)
975
+ expect(firstPage._unknown.length).toBe(0)
976
+ expect(firstPage.meta.lastEvaluatedKey).not.toBeUndefined()
977
+
978
+ const secondPage = await client.query(
979
+ {
980
+ KeyConditionExpression: `PK = :pk and begins_with(SK, :sk)`,
981
+ ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT#" },
982
+ Limit: 30,
983
+ ExclusiveStartKey: firstPage.meta.lastEvaluatedKey
984
+ },
985
+ { a: A, b: B }
986
+ )
987
+
988
+ expect(secondPage.a.length).toBe(0)
989
+ expect(secondPage.b.length).toBe(10)
990
+ expect(secondPage._unknown.length).toBe(0)
991
+ expect(secondPage.meta.lastEvaluatedKey).toBeUndefined()
992
+ })
993
+
994
+ test("it fetches all pages automatically", async () => {
995
+ await sandbox.seed(
996
+ ...Array.from({ length: 20 }).map(
997
+ (_, i) =>
998
+ new A({ pk: "abc", sk: `SORT#${String(i).padStart(2, "0")}`, a: i })
999
+ ),
1000
+ ...Array.from({ length: 20 }).map(
1001
+ (_, i) => new B({ pk: "abc", sk: `SORT#${i + 20}`, b: "bar" })
1002
+ )
1003
+ )
1004
+
1005
+ const { a, b, meta, _unknown } = await client.query(
1006
+ {
1007
+ KeyConditionExpression: `PK = :pk and begins_with(SK, :sk)`,
1008
+ ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT#" },
1009
+ FetchAllPages: true,
1010
+ // You wouldn't set a limit in a real-world use case here to optimize fetching all items.
1011
+ Limit: 10
1012
+ },
1013
+ { a: A, b: B }
1014
+ )
1015
+
1016
+ expect(a.length).toBe(20)
1017
+ expect(b.length).toBe(20)
1018
+ expect(_unknown.length).toBe(0)
1019
+ expect(meta.lastEvaluatedKey).toBeUndefined()
1020
+ })
1021
+ })
1022
+
1023
+ describe("bulk", () => {
1024
+ describe("< 25 elements (true transaction)", () => {
1025
+ test("it succeeds", async () => {
1026
+ const softDeleteTarget = new B({ pk: "PK#3", sk: "SK#3", b: "bar" })
1027
+
1028
+ await sandbox.seed(
1029
+ new A({ pk: "PK#1", sk: "SK#1", a: 1 }),
1030
+ new A({ pk: "PK#2", sk: "SK#2", a: 2 }),
1031
+ softDeleteTarget,
1032
+ new B({ pk: "PK#UPDATE", sk: "SK#UPDATE", b: "bar" }),
1033
+ new B({ pk: "PK#COND", sk: "SK#COND", b: "cond" })
1034
+ )
1035
+
1036
+ const before = await sandbox.snapshot()
1037
+
1038
+ await client.bulk([
1039
+ new A({ pk: "PK4", sk: "PK4", a: 4 }).operation("put"),
1040
+ A.operation("put", new A({ pk: "PK5", sk: "PK5", a: 5 })),
1041
+ new B({ pk: "PK6", sk: "SK6", b: "baz" }).operation("put"),
1042
+ A.operation("updateRaw", { PK: "PK#1", SK: "SK#1" }, { a: -1 }),
1043
+ new A({ pk: "PK#2", sk: "SK#2", a: 2 }).operation("delete"),
1044
+ B.operation("softDelete", softDeleteTarget),
1045
+ new B({
1046
+ pk: "PK#UPDATE",
1047
+ sk: "SK#UPDATE",
1048
+ b: "bar"
1049
+ }).operation("update", { b: "baz" }),
1050
+ new B({
1051
+ pk: "PK#COND",
1052
+ sk: "SK#COND",
1053
+ b: "cond"
1054
+ }).operation("condition", {
1055
+ ConditionExpression: "b = :cond",
1056
+ ExpressionAttributeValues: { ":cond": "cond" }
1057
+ })
1058
+ ])
1059
+
1060
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
1061
+ Snapshot Diff:
1062
+ - First value
1063
+ + Second value
1064
+
1065
+ @@ -1,32 +1,24 @@
1066
+ Object {
1067
+ + "$$DELETED$$PK#3__$$DELETED$$SK#3": Object {
1068
+ + "PK": "$$DELETED$$PK#3",
1069
+ + "SK": "$$DELETED$$SK#3",
1070
+ + "_deletedAt": "2021-05-01T08:00:00.000Z",
1071
+ + "_docVersion": 0,
1072
+ + "_tag": "B",
1073
+ + "b": "bar",
1074
+ + "pk": "PK#3",
1075
+ + "sk": "SK#3",
1076
+ + },
1077
+ "PK#1__SK#1": Object {
1078
+ "PK": "PK#1",
1079
+ "SK": "SK#1",
1080
+ "_docVersion": 0,
1081
+ "_tag": "A",
1082
+ - "a": 1,
1083
+ + "a": -1,
1084
+ "pk": "PK#1",
1085
+ "sk": "SK#1",
1086
+ - },
1087
+ - "PK#2__SK#2": Object {
1088
+ - "PK": "PK#2",
1089
+ - "SK": "SK#2",
1090
+ - "_docVersion": 0,
1091
+ - "_tag": "A",
1092
+ - "a": 2,
1093
+ - "pk": "PK#2",
1094
+ - "sk": "SK#2",
1095
+ - },
1096
+ - "PK#3__SK#3": Object {
1097
+ - "PK": "PK#3",
1098
+ - "SK": "SK#3",
1099
+ - "_docVersion": 0,
1100
+ - "_tag": "B",
1101
+ - "b": "bar",
1102
+ - "pk": "PK#3",
1103
+ - "sk": "SK#3",
1104
+ },
1105
+ "PK#COND__SK#COND": Object {
1106
+ "PK": "PK#COND",
1107
+ "SK": "SK#COND",
1108
+ "_docVersion": 0,
1109
+ @@ -36,12 +28,39 @@
1110
+ "sk": "SK#COND",
1111
+ },
1112
+ "PK#UPDATE__SK#UPDATE": Object {
1113
+ "PK": "PK#UPDATE",
1114
+ "SK": "SK#UPDATE",
1115
+ - "_docVersion": 0,
1116
+ + "_docVersion": 1,
1117
+ "_tag": "B",
1118
+ - "b": "bar",
1119
+ + "b": "baz",
1120
+ "pk": "PK#UPDATE",
1121
+ "sk": "SK#UPDATE",
1122
+ + },
1123
+ + "PK4__PK4": Object {
1124
+ + "PK": "PK4",
1125
+ + "SK": "PK4",
1126
+ + "_docVersion": 0,
1127
+ + "_tag": "A",
1128
+ + "a": 4,
1129
+ + "pk": "PK4",
1130
+ + "sk": "PK4",
1131
+ + },
1132
+ + "PK5__PK5": Object {
1133
+ + "PK": "PK5",
1134
+ + "SK": "PK5",
1135
+ + "_docVersion": 0,
1136
+ + "_tag": "A",
1137
+ + "a": 5,
1138
+ + "pk": "PK5",
1139
+ + "sk": "PK5",
1140
+ + },
1141
+ + "PK6__SK6": Object {
1142
+ + "PK": "PK6",
1143
+ + "SK": "SK6",
1144
+ + "_docVersion": 0,
1145
+ + "_tag": "B",
1146
+ + "b": "baz",
1147
+ + "pk": "PK6",
1148
+ + "sk": "SK6",
1149
+ },
1150
+ }
1151
+ `)
1152
+ })
1153
+
1154
+ test("it fails", async () => {
1155
+ await sandbox.seed(
1156
+ new A({ pk: "PK#1", sk: "SK#1", a: 1 }),
1157
+ new A({ pk: "PK#2", sk: "SK#2", a: 2 }),
1158
+ new B({ pk: "PK#3", sk: "SK#3", b: "bar" }),
1159
+ new B({ pk: "PK#UPDATE", sk: "SK#UPDATE", b: "bar" }),
1160
+ new B({ pk: "PK#COND", sk: "SK#COND", b: "cond" })
1161
+ )
1162
+
1163
+ const before = await sandbox.snapshot()
1164
+
1165
+ await expect(
1166
+ client.bulk([
1167
+ // Succeed
1168
+ new A({ pk: "PK#4", sk: "PK#4", a: 4 }).operation("put"),
1169
+ A.operation("put", new A({ pk: "PK5", sk: "PK5", a: 5 })),
1170
+ new B({ pk: "PK#6", sk: "SK#6", b: "baz" }).operation("put"),
1171
+
1172
+ // Fails
1173
+ A.operation(
1174
+ "updateRaw",
1175
+ { PK: "PK#nicetry", SK: "SK#nope" },
1176
+ { a: 234 }
1177
+ )
1178
+ ])
1179
+ ).rejects.toBeInstanceOf(BulkWriteTransactionError)
1180
+
1181
+ expect(await sandbox.snapshot()).toEqual(before)
1182
+ })
1183
+ })
1184
+
1185
+ describe("> 25 items (pseudo transaction)", () => {
1186
+ test("it succeeds", async () => {
1187
+ await sandbox.seed(
1188
+ new A({ pk: "PK#1", sk: "SK#1", a: 1 }),
1189
+ new A({ pk: "PK#2", sk: "SK#2", a: 2 }),
1190
+ new B({ pk: "PK#3", sk: "SK#3", b: "bar" })
1191
+ )
1192
+
1193
+ const before = await sandbox.snapshot()
1194
+
1195
+ await client.bulk([
1196
+ new A({ pk: "PK4", sk: "PK4", a: 4 }).operation("put"),
1197
+ A.operation("put", new A({ pk: "PK5", sk: "PK5", a: 5 })),
1198
+ new B({ pk: "PK6", sk: "SK6", b: "baz" }).operation("put"),
1199
+ A.operation("updateRaw", { PK: "PK#1", SK: "SK#1" }, { a: -1 }),
1200
+ new A({ pk: "PK#2", sk: "SK#2", a: 2 }).operation("delete"),
1201
+ B.operation("delete", { PK: "PK#3", SK: "SK#3" }),
1202
+ new B({
1203
+ pk: "PK#UPDATE",
1204
+ sk: "SK#UPDATE",
1205
+ b: "bar"
1206
+ }).operation("update", { b: "baz" }),
1207
+ ...Array.from({ length: 25 }).map((_, i) =>
1208
+ new A({ pk: `PK#A${i}`, sk: `SK#A${i}`, a: i }).operation("put")
1209
+ )
1210
+ ])
1211
+
1212
+ //#region snapshot
1213
+ expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
1214
+ Snapshot Diff:
1215
+ - First value
1216
+ + Second value
1217
+
1218
+ @@ -2,28 +2,271 @@
1219
+ "PK#1__SK#1": Object {
1220
+ "PK": "PK#1",
1221
+ "SK": "SK#1",
1222
+ "_docVersion": 0,
1223
+ "_tag": "A",
1224
+ - "a": 1,
1225
+ + "a": -1,
1226
+ "pk": "PK#1",
1227
+ "sk": "SK#1",
1228
+ + },
1229
+ + "PK#A0__SK#A0": Object {
1230
+ + "PK": "PK#A0",
1231
+ + "SK": "SK#A0",
1232
+ + "_docVersion": 0,
1233
+ + "_tag": "A",
1234
+ + "a": 0,
1235
+ + "pk": "PK#A0",
1236
+ + "sk": "SK#A0",
1237
+ + },
1238
+ + "PK#A10__SK#A10": Object {
1239
+ + "PK": "PK#A10",
1240
+ + "SK": "SK#A10",
1241
+ + "_docVersion": 0,
1242
+ + "_tag": "A",
1243
+ + "a": 10,
1244
+ + "pk": "PK#A10",
1245
+ + "sk": "SK#A10",
1246
+ + },
1247
+ + "PK#A11__SK#A11": Object {
1248
+ + "PK": "PK#A11",
1249
+ + "SK": "SK#A11",
1250
+ + "_docVersion": 0,
1251
+ + "_tag": "A",
1252
+ + "a": 11,
1253
+ + "pk": "PK#A11",
1254
+ + "sk": "SK#A11",
1255
+ + },
1256
+ + "PK#A12__SK#A12": Object {
1257
+ + "PK": "PK#A12",
1258
+ + "SK": "SK#A12",
1259
+ + "_docVersion": 0,
1260
+ + "_tag": "A",
1261
+ + "a": 12,
1262
+ + "pk": "PK#A12",
1263
+ + "sk": "SK#A12",
1264
+ + },
1265
+ + "PK#A13__SK#A13": Object {
1266
+ + "PK": "PK#A13",
1267
+ + "SK": "SK#A13",
1268
+ + "_docVersion": 0,
1269
+ + "_tag": "A",
1270
+ + "a": 13,
1271
+ + "pk": "PK#A13",
1272
+ + "sk": "SK#A13",
1273
+ + },
1274
+ + "PK#A14__SK#A14": Object {
1275
+ + "PK": "PK#A14",
1276
+ + "SK": "SK#A14",
1277
+ + "_docVersion": 0,
1278
+ + "_tag": "A",
1279
+ + "a": 14,
1280
+ + "pk": "PK#A14",
1281
+ + "sk": "SK#A14",
1282
+ },
1283
+ - "PK#2__SK#2": Object {
1284
+ - "PK": "PK#2",
1285
+ - "SK": "SK#2",
1286
+ + "PK#A15__SK#A15": Object {
1287
+ + "PK": "PK#A15",
1288
+ + "SK": "SK#A15",
1289
+ + "_docVersion": 0,
1290
+ + "_tag": "A",
1291
+ + "a": 15,
1292
+ + "pk": "PK#A15",
1293
+ + "sk": "SK#A15",
1294
+ + },
1295
+ + "PK#A16__SK#A16": Object {
1296
+ + "PK": "PK#A16",
1297
+ + "SK": "SK#A16",
1298
+ + "_docVersion": 0,
1299
+ + "_tag": "A",
1300
+ + "a": 16,
1301
+ + "pk": "PK#A16",
1302
+ + "sk": "SK#A16",
1303
+ + },
1304
+ + "PK#A17__SK#A17": Object {
1305
+ + "PK": "PK#A17",
1306
+ + "SK": "SK#A17",
1307
+ + "_docVersion": 0,
1308
+ + "_tag": "A",
1309
+ + "a": 17,
1310
+ + "pk": "PK#A17",
1311
+ + "sk": "SK#A17",
1312
+ + },
1313
+ + "PK#A18__SK#A18": Object {
1314
+ + "PK": "PK#A18",
1315
+ + "SK": "SK#A18",
1316
+ + "_docVersion": 0,
1317
+ + "_tag": "A",
1318
+ + "a": 18,
1319
+ + "pk": "PK#A18",
1320
+ + "sk": "SK#A18",
1321
+ + },
1322
+ + "PK#A19__SK#A19": Object {
1323
+ + "PK": "PK#A19",
1324
+ + "SK": "SK#A19",
1325
+ + "_docVersion": 0,
1326
+ + "_tag": "A",
1327
+ + "a": 19,
1328
+ + "pk": "PK#A19",
1329
+ + "sk": "SK#A19",
1330
+ + },
1331
+ + "PK#A1__SK#A1": Object {
1332
+ + "PK": "PK#A1",
1333
+ + "SK": "SK#A1",
1334
+ + "_docVersion": 0,
1335
+ + "_tag": "A",
1336
+ + "a": 1,
1337
+ + "pk": "PK#A1",
1338
+ + "sk": "SK#A1",
1339
+ + },
1340
+ + "PK#A20__SK#A20": Object {
1341
+ + "PK": "PK#A20",
1342
+ + "SK": "SK#A20",
1343
+ + "_docVersion": 0,
1344
+ + "_tag": "A",
1345
+ + "a": 20,
1346
+ + "pk": "PK#A20",
1347
+ + "sk": "SK#A20",
1348
+ + },
1349
+ + "PK#A21__SK#A21": Object {
1350
+ + "PK": "PK#A21",
1351
+ + "SK": "SK#A21",
1352
+ + "_docVersion": 0,
1353
+ + "_tag": "A",
1354
+ + "a": 21,
1355
+ + "pk": "PK#A21",
1356
+ + "sk": "SK#A21",
1357
+ + },
1358
+ + "PK#A22__SK#A22": Object {
1359
+ + "PK": "PK#A22",
1360
+ + "SK": "SK#A22",
1361
+ + "_docVersion": 0,
1362
+ + "_tag": "A",
1363
+ + "a": 22,
1364
+ + "pk": "PK#A22",
1365
+ + "sk": "SK#A22",
1366
+ + },
1367
+ + "PK#A23__SK#A23": Object {
1368
+ + "PK": "PK#A23",
1369
+ + "SK": "SK#A23",
1370
+ + "_docVersion": 0,
1371
+ + "_tag": "A",
1372
+ + "a": 23,
1373
+ + "pk": "PK#A23",
1374
+ + "sk": "SK#A23",
1375
+ + },
1376
+ + "PK#A24__SK#A24": Object {
1377
+ + "PK": "PK#A24",
1378
+ + "SK": "SK#A24",
1379
+ + "_docVersion": 0,
1380
+ + "_tag": "A",
1381
+ + "a": 24,
1382
+ + "pk": "PK#A24",
1383
+ + "sk": "SK#A24",
1384
+ + },
1385
+ + "PK#A2__SK#A2": Object {
1386
+ + "PK": "PK#A2",
1387
+ + "SK": "SK#A2",
1388
+ "_docVersion": 0,
1389
+ "_tag": "A",
1390
+ "a": 2,
1391
+ - "pk": "PK#2",
1392
+ - "sk": "SK#2",
1393
+ + "pk": "PK#A2",
1394
+ + "sk": "SK#A2",
1395
+ + },
1396
+ + "PK#A3__SK#A3": Object {
1397
+ + "PK": "PK#A3",
1398
+ + "SK": "SK#A3",
1399
+ + "_docVersion": 0,
1400
+ + "_tag": "A",
1401
+ + "a": 3,
1402
+ + "pk": "PK#A3",
1403
+ + "sk": "SK#A3",
1404
+ + },
1405
+ + "PK#A4__SK#A4": Object {
1406
+ + "PK": "PK#A4",
1407
+ + "SK": "SK#A4",
1408
+ + "_docVersion": 0,
1409
+ + "_tag": "A",
1410
+ + "a": 4,
1411
+ + "pk": "PK#A4",
1412
+ + "sk": "SK#A4",
1413
+ + },
1414
+ + "PK#A5__SK#A5": Object {
1415
+ + "PK": "PK#A5",
1416
+ + "SK": "SK#A5",
1417
+ + "_docVersion": 0,
1418
+ + "_tag": "A",
1419
+ + "a": 5,
1420
+ + "pk": "PK#A5",
1421
+ + "sk": "SK#A5",
1422
+ + },
1423
+ + "PK#A6__SK#A6": Object {
1424
+ + "PK": "PK#A6",
1425
+ + "SK": "SK#A6",
1426
+ + "_docVersion": 0,
1427
+ + "_tag": "A",
1428
+ + "a": 6,
1429
+ + "pk": "PK#A6",
1430
+ + "sk": "SK#A6",
1431
+ + },
1432
+ + "PK#A7__SK#A7": Object {
1433
+ + "PK": "PK#A7",
1434
+ + "SK": "SK#A7",
1435
+ + "_docVersion": 0,
1436
+ + "_tag": "A",
1437
+ + "a": 7,
1438
+ + "pk": "PK#A7",
1439
+ + "sk": "SK#A7",
1440
+ + },
1441
+ + "PK#A8__SK#A8": Object {
1442
+ + "PK": "PK#A8",
1443
+ + "SK": "SK#A8",
1444
+ + "_docVersion": 0,
1445
+ + "_tag": "A",
1446
+ + "a": 8,
1447
+ + "pk": "PK#A8",
1448
+ + "sk": "SK#A8",
1449
+ + },
1450
+ + "PK#A9__SK#A9": Object {
1451
+ + "PK": "PK#A9",
1452
+ + "SK": "SK#A9",
1453
+ + "_docVersion": 0,
1454
+ + "_tag": "A",
1455
+ + "a": 9,
1456
+ + "pk": "PK#A9",
1457
+ + "sk": "SK#A9",
1458
+ },
1459
+ - "PK#3__SK#3": Object {
1460
+ - "PK": "PK#3",
1461
+ - "SK": "SK#3",
1462
+ + "PK#UPDATE__SK#UPDATE": Object {
1463
+ + "PK": "PK#UPDATE",
1464
+ + "SK": "SK#UPDATE",
1465
+ + "_docVersion": 1,
1466
+ + "_tag": "B",
1467
+ + "b": "baz",
1468
+ + "pk": "PK#UPDATE",
1469
+ + "sk": "SK#UPDATE",
1470
+ + },
1471
+ + "PK4__PK4": Object {
1472
+ + "PK": "PK4",
1473
+ + "SK": "PK4",
1474
+ + "_docVersion": 0,
1475
+ + "_tag": "A",
1476
+ + "a": 4,
1477
+ + "pk": "PK4",
1478
+ + "sk": "PK4",
1479
+ + },
1480
+ + "PK5__PK5": Object {
1481
+ + "PK": "PK5",
1482
+ + "SK": "PK5",
1483
+ "_docVersion": 0,
1484
+ + "_tag": "A",
1485
+ + "a": 5,
1486
+ + "pk": "PK5",
1487
+ + "sk": "PK5",
1488
+ + },
1489
+ + "PK6__SK6": Object {
1490
+ + "PK": "PK6",
1491
+ + "SK": "SK6",
1492
+ + "_docVersion": 0,
1493
+ "_tag": "B",
1494
+ - "b": "bar",
1495
+ - "pk": "PK#3",
1496
+ - "sk": "SK#3",
1497
+ + "b": "baz",
1498
+ + "pk": "PK6",
1499
+ + "sk": "SK6",
1500
+ },
1501
+ }
1502
+ `)
1503
+ //#endregion
1504
+ })
1505
+
1506
+ test("it fails and rolls back", async () => {
1507
+ const before = await sandbox.snapshot()
1508
+
1509
+ await expect(
1510
+ client.bulk([
1511
+ // Succeeds
1512
+ ...Array.from({ length: 40 }).map((_, i) =>
1513
+ new A({ pk: `PK#${i}`, sk: `SK#${i}`, a: i }).operation("put")
1514
+ ),
1515
+
1516
+ // Fails
1517
+ A.operation(
1518
+ "condition",
1519
+ { PK: "nicetry", SK: "nope" },
1520
+ { ConditionExpression: "attribute_exists(PK)" }
1521
+ )
1522
+ ])
1523
+ ).rejects.toBeInstanceOf(BulkWriteTransactionError)
1524
+
1525
+ expect(await sandbox.snapshot()).toEqual(before)
1526
+ })
1527
+ })
1528
+ })
1529
+
1530
+ describe("batchGet", () => {
1531
+ class A extends model(
1532
+ "A",
1533
+ t.type({ pk: t.string, sk: t.string, a: t.number }),
1534
+ provider
1535
+ ) {
1536
+ get PK() {
1537
+ return this.pk
1538
+ }
1539
+ get SK() {
1540
+ return this.sk
1541
+ }
1542
+ }
1543
+
1544
+ test("it fetches an empty record", async () => {
1545
+ expect(await client.batchGet({})).toEqual({})
1546
+ })
1547
+
1548
+ test("it throws if some items don't exist", async () => {
1549
+ await expect(
1550
+ client.batchGet({
1551
+ one: A.operation("get", { PK: "PK#1", SK: "SK#1" }),
1552
+ two: A.operation("get", { PK: "PK#2", SK: "SK#2" }),
1553
+ three: A.operation("get", { PK: "PK#3", SK: "SK#3" }),
1554
+ four: A.operation("get", { PK: "PK#4", SK: "SK#4" }),
1555
+ duplicate: A.operation("get", { PK: "PK#1", SK: "SK#1" })
1556
+ })
1557
+ ).rejects.toBeInstanceOf(ItemNotFoundError)
1558
+ })
1559
+
1560
+ test("it returns individual errors", async () => {
1561
+ await sandbox.seed(
1562
+ new A({ pk: "PK#1", sk: "SK#1", a: 1 }),
1563
+ new A({ pk: "PK#2", sk: "SK#2", a: 2 })
1564
+ )
1565
+
1566
+ const result = await client.batchGet(
1567
+ {
1568
+ one: A.operation("get", { PK: "PK#1", SK: "SK#1" }),
1569
+ two: A.operation("get", { PK: "PK#2", SK: "SK#2" }),
1570
+ duplicate: A.operation("get", { PK: "PK#1", SK: "SK#1" }),
1571
+ error: A.operation("get", { PK: "PK#error", SK: "SK#error" }),
1572
+ error2: A.operation("get", { PK: "PK#error2", SK: "SK#error2" })
1573
+ },
1574
+ { individualErrors: true }
1575
+ )
1576
+
1577
+ expect(result.one).toBeInstanceOf(A)
1578
+ expect(result.two).toBeInstanceOf(A)
1579
+ expect(result.duplicate).toBeInstanceOf(A)
1580
+ expect(result.error).toBeInstanceOf(ItemNotFoundError)
1581
+ })
1582
+
1583
+ test("it fetches <=100 entries in one go", async () => {
1584
+ await sandbox.seed(
1585
+ new A({ pk: "PK#1", sk: "SK#1", a: 1 }),
1586
+ new A({ pk: "PK#2", sk: "SK#2", a: 2 }),
1587
+ new A({ pk: "PK#3", sk: "SK#3", a: 3 }),
1588
+ new A({ pk: "PK#4", sk: "SK#4", a: 4 })
1589
+ )
1590
+
1591
+ const results = await client.batchGet({
1592
+ one: A.operation("get", { PK: "PK#1", SK: "SK#1" }),
1593
+ two: A.operation("get", { PK: "PK#2", SK: "SK#2" }),
1594
+ three: A.operation("get", { PK: "PK#3", SK: "SK#3" }),
1595
+ four: A.operation("get", { PK: "PK#4", SK: "SK#4" }),
1596
+ duplicate: A.operation("get", { PK: "PK#1", SK: "SK#1" })
1597
+ })
1598
+
1599
+ expect(
1600
+ Object.fromEntries(
1601
+ Object.entries(results).map(([key, val]) => [key, val.values()])
1602
+ )
1603
+ ).toMatchInlineSnapshot(`
1604
+ Object {
1605
+ "duplicate": Object {
1606
+ "a": 1,
1607
+ "pk": "PK#1",
1608
+ "sk": "SK#1",
1609
+ },
1610
+ "four": Object {
1611
+ "a": 4,
1612
+ "pk": "PK#4",
1613
+ "sk": "SK#4",
1614
+ },
1615
+ "one": Object {
1616
+ "a": 1,
1617
+ "pk": "PK#1",
1618
+ "sk": "SK#1",
1619
+ },
1620
+ "three": Object {
1621
+ "a": 3,
1622
+ "pk": "PK#3",
1623
+ "sk": "SK#3",
1624
+ },
1625
+ "two": Object {
1626
+ "a": 2,
1627
+ "pk": "PK#2",
1628
+ "sk": "SK#2",
1629
+ },
1630
+ }
1631
+ `)
1632
+ })
1633
+ })
1634
+
1635
+ describe("load", () => {
1636
+ describe("client", () => {
1637
+ test("it throws if item doesn't exist", async () => {
1638
+ await expect(
1639
+ client.load(A.operation("get", { PK: "PK", SK: "SK" }))
1640
+ ).rejects.toBeInstanceOf(ItemNotFoundError)
1641
+ })
1642
+
1643
+ test("it returns null instead of throwing if item doesn't exist", async () => {
1644
+ await expect(
1645
+ client.load(A.operation("get", { PK: "PK", SK: "SK" }), { null: true })
1646
+ ).resolves.toBeNull()
1647
+ })
1648
+
1649
+ test("it fetches >100 items", async () => {
1650
+ const items = Array.from({ length: 234 }).map((_, i) =>
1651
+ i < 100
1652
+ ? new A({ pk: String(i), sk: String(i), a: i })
1653
+ : new B({ pk: String(i), sk: String(i), b: String(i) })
1654
+ )
1655
+
1656
+ const spy = jest.spyOn(client, "batchGet")
1657
+
1658
+ await sandbox.seed(...items)
1659
+
1660
+ const results = await Promise.all<A | B>(
1661
+ items.map(({ PK, SK }, i) =>
1662
+ i < 100
1663
+ ? client.load(A.operation("get", { PK, SK }))
1664
+ : client.load(B.operation("get", { PK, SK }))
1665
+ )
1666
+ )
1667
+
1668
+ expect(results.length).toBe(234)
1669
+ expect(spy).toHaveBeenCalledTimes(3)
1670
+
1671
+ spy.mockReset()
1672
+ spy.mockRestore()
1673
+ })
1674
+ })
1675
+
1676
+ describe("model", () => {
1677
+ test("it throws if item doesn't exist", async () => {
1678
+ await expect(A.load({ PK: "PK", SK: "SK" })).rejects.toBeInstanceOf(
1679
+ ItemNotFoundError
1680
+ )
1681
+ })
1682
+
1683
+ test("it returns null instead of throwing if item doesn't exist", async () => {
1684
+ await expect(
1685
+ A.load({ PK: "PK", SK: "SK" }, { null: true })
1686
+ ).resolves.toBeNull()
1687
+ })
1688
+
1689
+ test("it fetches >100 items", async () => {
1690
+ const items = Array.from({ length: 234 }).map(
1691
+ (_, i) => new A({ pk: String(i), sk: String(i), a: i })
1692
+ )
1693
+
1694
+ const spy = jest.spyOn(client, "batchGet")
1695
+
1696
+ await sandbox.seed(...items)
1697
+
1698
+ const results = await Promise.all<A | B>(
1699
+ items.map(({ PK, SK }, i) => A.load({ PK, SK }))
1700
+ )
1701
+
1702
+ expect(results.length).toBe(234)
1703
+ expect(spy).toHaveBeenCalledTimes(3)
1704
+
1705
+ spy.mockReset()
1706
+ spy.mockRestore()
1707
+ })
1708
+ })
1709
+
1710
+ describe("union", () => {
1711
+ test("it throws if item doesn't exist", async () => {
1712
+ await expect(Union.load({ PK: "PK", SK: "SK" })).rejects.toBeInstanceOf(
1713
+ ItemNotFoundError
1714
+ )
1715
+ })
1716
+
1717
+ test("it returns null instead of throwing if item doesn't exist", async () => {
1718
+ await expect(
1719
+ Union.load({ PK: "PK", SK: "SK" }, { null: true })
1720
+ ).resolves.toBeNull()
1721
+ })
1722
+
1723
+ test("it fetches >100 items", async () => {
1724
+ const items = Array.from({ length: 234 }).map((_, i) =>
1725
+ i < 123
1726
+ ? new C({ pk: String(i), sk: String(i), c: String(i) })
1727
+ : new D({ pk: String(i), sk: String(i), d: String(i) })
1728
+ )
1729
+
1730
+ const spy = jest.spyOn(client, "batchGet")
1731
+
1732
+ await sandbox.seed(...items)
1733
+
1734
+ const results = await Promise.all(
1735
+ items.map(({ PK, SK }, i) => Union.load({ PK, SK }))
1736
+ )
1737
+
1738
+ expect(results.length).toBe(234)
1739
+ expect(results.filter(item => item instanceof C).length).toBe(123)
1740
+ expect(results.filter(item => item instanceof D).length).toBe(111)
1741
+ expect(spy).toHaveBeenCalledTimes(3)
1742
+
1743
+ spy.mockReset()
1744
+ spy.mockRestore()
1745
+ })
1746
+ })
1747
+ })
1748
+
1749
+ describe("loadMany", () => {
1750
+ describe("client", () => {
1751
+ test("it fetches >100 items", async () => {
1752
+ const items = Array.from({ length: 234 }).map((_, i) =>
1753
+ i < 100
1754
+ ? new A({ pk: String(i), sk: String(i), a: i })
1755
+ : new B({ pk: String(i), sk: String(i), b: String(i) })
1756
+ )
1757
+
1758
+ const spy = jest.spyOn(client, "batchGet")
1759
+
1760
+ await sandbox.seed(...items)
1761
+
1762
+ const results = await client.loadMany<typeof A | typeof B>(
1763
+ items.map(({ PK, SK }, i) =>
1764
+ i < 100
1765
+ ? A.operation("get", { PK, SK })
1766
+ : B.operation("get", { PK, SK })
1767
+ )
1768
+ )
1769
+
1770
+ expect(results.length).toBe(234)
1771
+ expect(spy).toHaveBeenCalledTimes(3)
1772
+
1773
+ spy.mockReset()
1774
+ spy.mockRestore()
1775
+ })
1776
+ })
1777
+
1778
+ describe("model", () => {
1779
+ test("it fetches >100 items", async () => {
1780
+ const items = Array.from({ length: 234 }).map(
1781
+ (_, i) => new A({ pk: String(i), sk: String(i), a: i })
1782
+ )
1783
+
1784
+ const spy = jest.spyOn(client, "batchGet")
1785
+
1786
+ await sandbox.seed(...items)
1787
+
1788
+ const results = await A.loadMany(items.map(({ PK, SK }) => ({ PK, SK })))
1789
+
1790
+ expect(results.length).toBe(234)
1791
+ expect(spy).toHaveBeenCalledTimes(3)
1792
+
1793
+ spy.mockReset()
1794
+ spy.mockRestore()
1795
+ })
1796
+ })
1797
+
1798
+ describe("union", () => {
1799
+ test("it fetches >100 items", async () => {
1800
+ const items = Array.from({ length: 234 }).map((_, i) =>
1801
+ i < 123
1802
+ ? new C({ pk: String(i), sk: String(i), c: String(i) })
1803
+ : new D({ pk: String(i), sk: String(i), d: String(i) })
1804
+ )
1805
+
1806
+ const spy = jest.spyOn(client, "batchGet")
1807
+
1808
+ await sandbox.seed(...items)
1809
+
1810
+ const results = await Union.loadMany(
1811
+ items.map(({ PK, SK }) => ({ PK, SK }))
1812
+ )
1813
+
1814
+ expect(results.length).toBe(234)
1815
+ expect(results.filter(item => item instanceof C).length).toBe(123)
1816
+ expect(results.filter(item => item instanceof D).length).toBe(111)
1817
+ expect(spy).toHaveBeenCalledTimes(3)
1818
+
1819
+ spy.mockReset()
1820
+ spy.mockRestore()
1821
+ })
1822
+ })
1823
+ })
1824
+
1825
+ describe("paginate", () => {
1826
+ describe("client", () => {
1827
+ test("it paginates a regular model", async () => {
1828
+ const items = Array.from({ length: 60 }).map(
1829
+ (_, i) =>
1830
+ new C({ pk: "PK", sk: String(i).padStart(3, "0"), c: String(i) })
1831
+ )
1832
+
1833
+ await sandbox.seed(...items)
1834
+
1835
+ // Forwards
1836
+ const page1 = await client.paginate(
1837
+ C,
1838
+ {},
1839
+ {
1840
+ KeyConditionExpression: "PK = :pk",
1841
+ ExpressionAttributeValues: { ":pk": "PK" }
1842
+ }
1843
+ )
1844
+ expect(page1.pageInfo).toMatchInlineSnapshot(`
1845
+ Object {
1846
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo7vfn9aOeA8=",
1847
+ "hasNextPage": true,
1848
+ "hasPreviousPage": false,
1849
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6v1n9aOeA8=",
1850
+ }
1851
+ `)
1852
+ expect(page1.edges.length).toBe(20)
1853
+ expect(page1.edges[0].node.c).toBe("0")
1854
+ expect(page1.edges[19].node.c).toBe("19")
1855
+
1856
+ const page2 = await client.paginate(
1857
+ C,
1858
+ { after: page1.pageInfo.endCursor },
1859
+ {
1860
+ KeyConditionExpression: "PK = :pk",
1861
+ ExpressionAttributeValues: { ":pk": "PK" }
1862
+ }
1863
+ )
1864
+ expect(page2.pageInfo).toMatchInlineSnapshot(`
1865
+ Object {
1866
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo5Xfn9aOeA8=",
1867
+ "hasNextPage": true,
1868
+ "hasPreviousPage": false,
1869
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo4X1n9aOeA8=",
1870
+ }
1871
+ `)
1872
+ expect(page2.edges.length).toBe(20)
1873
+ expect(page2.edges[0].node.c).toBe("20")
1874
+ expect(page2.edges[19].node.c).toBe("39")
1875
+
1876
+ const page3 = await client.paginate(
1877
+ C,
1878
+ { after: page2.pageInfo.endCursor },
1879
+ {
1880
+ KeyConditionExpression: "PK = :pk",
1881
+ ExpressionAttributeValues: { ":pk": "PK" }
1882
+ }
1883
+ )
1884
+ expect(page3.pageInfo).toMatchInlineSnapshot(`
1885
+ Object {
1886
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOoLvfn9aOeA8=",
1887
+ "hasNextPage": false,
1888
+ "hasPreviousPage": false,
1889
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOoKv1n9aOeA8=",
1890
+ }
1891
+ `)
1892
+ expect(page3.edges.length).toBe(20)
1893
+ expect(page3.edges[0].node.c).toBe("40")
1894
+ expect(page3.edges[19].node.c).toBe("59")
1895
+
1896
+ // Backwards
1897
+ const backwardsPage2 = await client.paginate(
1898
+ C,
1899
+ { before: page3.pageInfo.startCursor },
1900
+ {
1901
+ KeyConditionExpression: "PK = :pk",
1902
+ ExpressionAttributeValues: { ":pk": "PK" }
1903
+ }
1904
+ )
1905
+ expect(backwardsPage2.pageInfo).toMatchInlineSnapshot(`
1906
+ Object {
1907
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo5Xfn9aOeA8=",
1908
+ "hasNextPage": false,
1909
+ "hasPreviousPage": true,
1910
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo4X1n9aOeA8=",
1911
+ }
1912
+ `)
1913
+ expect(backwardsPage2.edges.length).toBe(20)
1914
+ expect(backwardsPage2.edges[0].node.c).toBe("20")
1915
+ expect(backwardsPage2.edges[19].node.c).toBe("39")
1916
+
1917
+ const backwardsPage1 = await client.paginate(
1918
+ C,
1919
+ { before: backwardsPage2.pageInfo.startCursor },
1920
+ {
1921
+ KeyConditionExpression: "PK = :pk",
1922
+ ExpressionAttributeValues: { ":pk": "PK" }
1923
+ }
1924
+ )
1925
+ expect(backwardsPage1.pageInfo).toMatchInlineSnapshot(`
1926
+ Object {
1927
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo7vfn9aOeA8=",
1928
+ "hasNextPage": false,
1929
+ "hasPreviousPage": false,
1930
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6v1n9aOeA8=",
1931
+ }
1932
+ `)
1933
+ expect(backwardsPage1.edges.length).toBe(20)
1934
+ expect(backwardsPage1.edges[0].node.c).toBe("0")
1935
+ expect(backwardsPage1.edges[19].node.c).toBe("19")
1936
+ })
1937
+
1938
+ test("it paginates a union model", async () => {
1939
+ const items = Array.from({ length: 60 }).map((_, i) =>
1940
+ i > 30
1941
+ ? new C({ pk: "PK", sk: String(i).padStart(3, "0"), c: String(i) })
1942
+ : new D({ pk: "PK", sk: String(i).padStart(3, "0"), d: String(i) })
1943
+ )
1944
+
1945
+ await sandbox.seed(...items)
1946
+
1947
+ // Forwards
1948
+ const page1 = await client.paginate(
1949
+ Union,
1950
+ {},
1951
+ {
1952
+ KeyConditionExpression: "PK = :pk",
1953
+ ExpressionAttributeValues: { ":pk": "PK" }
1954
+ }
1955
+ )
1956
+ expect(page1.pageInfo).toMatchInlineSnapshot(`
1957
+ Object {
1958
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo7vfn9aOeA8=",
1959
+ "hasNextPage": true,
1960
+ "hasPreviousPage": false,
1961
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6v1n9aOeA8=",
1962
+ }
1963
+ `)
1964
+ expect(page1.edges.length).toBe(20)
1965
+ expect(page1.edges[0].node.SK).toBe("000")
1966
+ expect(page1.edges[19].node.SK).toBe("019")
1967
+
1968
+ const page2 = await client.paginate(
1969
+ Union,
1970
+ { after: page1.pageInfo.endCursor },
1971
+ {
1972
+ KeyConditionExpression: "PK = :pk",
1973
+ ExpressionAttributeValues: { ":pk": "PK" }
1974
+ }
1975
+ )
1976
+ expect(page2.pageInfo).toMatchInlineSnapshot(`
1977
+ Object {
1978
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo5Xfn9aOeA8=",
1979
+ "hasNextPage": true,
1980
+ "hasPreviousPage": false,
1981
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo4X1n9aOeA8=",
1982
+ }
1983
+ `)
1984
+ expect(page2.edges.length).toBe(20)
1985
+ expect(page2.edges[0].node.SK).toBe("020")
1986
+ expect(page2.edges[19].node.SK).toBe("039")
1987
+
1988
+ const page3 = await client.paginate(
1989
+ Union,
1990
+ { after: page2.pageInfo.endCursor },
1991
+ {
1992
+ KeyConditionExpression: "PK = :pk",
1993
+ ExpressionAttributeValues: { ":pk": "PK" }
1994
+ }
1995
+ )
1996
+ expect(page3.pageInfo).toMatchInlineSnapshot(`
1997
+ Object {
1998
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOoLvfn9aOeA8=",
1999
+ "hasNextPage": false,
2000
+ "hasPreviousPage": false,
2001
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOoKv1n9aOeA8=",
2002
+ }
2003
+ `)
2004
+ expect(page3.edges.length).toBe(20)
2005
+ expect(page3.edges[0].node.SK).toBe("040")
2006
+ expect(page3.edges[19].node.SK).toBe("059")
2007
+
2008
+ // Backwards
2009
+ const backwardsPage2 = await client.paginate(
2010
+ Union,
2011
+ { before: page3.pageInfo.startCursor },
2012
+ {
2013
+ KeyConditionExpression: "PK = :pk",
2014
+ ExpressionAttributeValues: { ":pk": "PK" }
2015
+ }
2016
+ )
2017
+ expect(backwardsPage2.pageInfo).toMatchInlineSnapshot(`
2018
+ Object {
2019
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo5Xfn9aOeA8=",
2020
+ "hasNextPage": false,
2021
+ "hasPreviousPage": true,
2022
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo4X1n9aOeA8=",
2023
+ }
2024
+ `)
2025
+ expect(backwardsPage2.edges.length).toBe(20)
2026
+ expect(backwardsPage2.edges[0].node.SK).toBe("020")
2027
+ expect(backwardsPage2.edges[19].node.SK).toBe("039")
2028
+
2029
+ const backwardsPage1 = await client.paginate(
2030
+ Union,
2031
+ { before: backwardsPage2.pageInfo.startCursor },
2032
+ {
2033
+ KeyConditionExpression: "PK = :pk",
2034
+ ExpressionAttributeValues: { ":pk": "PK" }
2035
+ }
2036
+ )
2037
+ expect(backwardsPage1.pageInfo).toMatchInlineSnapshot(`
2038
+ Object {
2039
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo7vfn9aOeA8=",
2040
+ "hasNextPage": false,
2041
+ "hasPreviousPage": false,
2042
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6v1n9aOeA8=",
2043
+ }
2044
+ `)
2045
+ expect(backwardsPage1.edges.length).toBe(20)
2046
+ expect(backwardsPage1.edges[0].node.SK).toBe("000")
2047
+ expect(backwardsPage1.edges[19].node.SK).toBe("019")
2048
+ })
2049
+
2050
+ test("it respects a limit", async () => {
2051
+ const items = Array.from({ length: 60 }).map(
2052
+ (_, i) =>
2053
+ new C({ pk: "PK", sk: String(i).padStart(3, "0"), c: String(i) })
2054
+ )
2055
+
2056
+ await sandbox.seed(...items)
2057
+
2058
+ // Forwards
2059
+ const page = await client.paginate(
2060
+ C,
2061
+ { first: 10 },
2062
+ {
2063
+ KeyConditionExpression: "PK = :pk",
2064
+ ExpressionAttributeValues: { ":pk": "PK" }
2065
+ }
2066
+ )
2067
+ expect(page.pageInfo).toMatchInlineSnapshot(`
2068
+ Object {
2069
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6vfn9aOeA8=",
2070
+ "hasNextPage": true,
2071
+ "hasPreviousPage": false,
2072
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6v1n9aOeA8=",
2073
+ }
2074
+ `)
2075
+ expect(page.edges.length).toBe(10)
2076
+ expect(page.edges[0].node.c).toBe("0")
2077
+ expect(page.edges[9].node.c).toBe("9")
2078
+ })
2079
+
2080
+ test("it doesn't exceed the max limit", async () => {
2081
+ const items = Array.from({ length: 60 }).map(
2082
+ (_, i) =>
2083
+ new C({ pk: "PK", sk: String(i).padStart(3, "0"), c: String(i) })
2084
+ )
2085
+
2086
+ await sandbox.seed(...items)
2087
+
2088
+ // Forwards
2089
+ const page1 = await client.paginate(
2090
+ C,
2091
+ { first: 60 },
2092
+ {
2093
+ KeyConditionExpression: "PK = :pk",
2094
+ ExpressionAttributeValues: { ":pk": "PK" }
2095
+ }
2096
+ )
2097
+ expect(page1.pageInfo).toMatchInlineSnapshot(`
2098
+ Object {
2099
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOoKvfn9aOeA8=",
2100
+ "hasNextPage": true,
2101
+ "hasPreviousPage": false,
2102
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6v1n9aOeA8=",
2103
+ }
2104
+ `)
2105
+ expect(page1.edges.length).toBe(50)
2106
+ expect(page1.edges[0].node.c).toBe("0")
2107
+ expect(page1.edges[49].node.c).toBe("49")
2108
+ })
2109
+ })
2110
+
2111
+ describe("model", () => {
2112
+ test("it paginates a regular model", async () => {
2113
+ const items = Array.from({ length: 60 }).map(
2114
+ (_, i) =>
2115
+ new C({ pk: "PK", sk: String(i).padStart(3, "0"), c: String(i) })
2116
+ )
2117
+
2118
+ await sandbox.seed(...items)
2119
+
2120
+ // Forwards
2121
+ const page1 = await C.paginate(
2122
+ {},
2123
+ {
2124
+ KeyConditionExpression: "PK = :pk",
2125
+ ExpressionAttributeValues: { ":pk": "PK" }
2126
+ }
2127
+ )
2128
+ expect(page1.pageInfo).toMatchInlineSnapshot(`
2129
+ Object {
2130
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo7vfn9aOeA8=",
2131
+ "hasNextPage": true,
2132
+ "hasPreviousPage": false,
2133
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6v1n9aOeA8=",
2134
+ }
2135
+ `)
2136
+ expect(page1.edges.length).toBe(20)
2137
+ expect(page1.edges[0].node.c).toBe("0")
2138
+ expect(page1.edges[19].node.c).toBe("19")
2139
+
2140
+ const page2 = await C.paginate(
2141
+ { after: page1.pageInfo.endCursor },
2142
+ {
2143
+ KeyConditionExpression: "PK = :pk",
2144
+ ExpressionAttributeValues: { ":pk": "PK" }
2145
+ }
2146
+ )
2147
+ expect(page2.pageInfo).toMatchInlineSnapshot(`
2148
+ Object {
2149
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo5Xfn9aOeA8=",
2150
+ "hasNextPage": true,
2151
+ "hasPreviousPage": false,
2152
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo4X1n9aOeA8=",
2153
+ }
2154
+ `)
2155
+ expect(page2.edges.length).toBe(20)
2156
+ expect(page2.edges[0].node.c).toBe("20")
2157
+ expect(page2.edges[19].node.c).toBe("39")
2158
+
2159
+ const page3 = await C.paginate(
2160
+ { after: page2.pageInfo.endCursor },
2161
+ {
2162
+ KeyConditionExpression: "PK = :pk",
2163
+ ExpressionAttributeValues: { ":pk": "PK" }
2164
+ }
2165
+ )
2166
+ expect(page3.pageInfo).toMatchInlineSnapshot(`
2167
+ Object {
2168
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOoLvfn9aOeA8=",
2169
+ "hasNextPage": false,
2170
+ "hasPreviousPage": false,
2171
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOoKv1n9aOeA8=",
2172
+ }
2173
+ `)
2174
+ expect(page3.edges.length).toBe(20)
2175
+ expect(page3.edges[0].node.c).toBe("40")
2176
+ expect(page3.edges[19].node.c).toBe("59")
2177
+
2178
+ // Backwards
2179
+ const backwardsPage2 = await C.paginate(
2180
+ { before: page3.pageInfo.startCursor },
2181
+ {
2182
+ KeyConditionExpression: "PK = :pk",
2183
+ ExpressionAttributeValues: { ":pk": "PK" }
2184
+ }
2185
+ )
2186
+ expect(backwardsPage2.pageInfo).toMatchInlineSnapshot(`
2187
+ Object {
2188
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo5Xfn9aOeA8=",
2189
+ "hasNextPage": false,
2190
+ "hasPreviousPage": true,
2191
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo4X1n9aOeA8=",
2192
+ }
2193
+ `)
2194
+ expect(backwardsPage2.edges.length).toBe(20)
2195
+ expect(backwardsPage2.edges[0].node.c).toBe("20")
2196
+ expect(backwardsPage2.edges[19].node.c).toBe("39")
2197
+
2198
+ const backwardsPage1 = await C.paginate(
2199
+ { before: backwardsPage2.pageInfo.startCursor },
2200
+ {
2201
+ KeyConditionExpression: "PK = :pk",
2202
+ ExpressionAttributeValues: { ":pk": "PK" }
2203
+ }
2204
+ )
2205
+ expect(backwardsPage1.pageInfo).toMatchInlineSnapshot(`
2206
+ Object {
2207
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo7vfn9aOeA8=",
2208
+ "hasNextPage": false,
2209
+ "hasPreviousPage": false,
2210
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6v1n9aOeA8=",
2211
+ }
2212
+ `)
2213
+ expect(backwardsPage1.edges.length).toBe(20)
2214
+ expect(backwardsPage1.edges[0].node.c).toBe("0")
2215
+ expect(backwardsPage1.edges[19].node.c).toBe("19")
2216
+ })
2217
+
2218
+ test("it respects a limit", async () => {
2219
+ const items = Array.from({ length: 60 }).map(
2220
+ (_, i) =>
2221
+ new C({ pk: "PK", sk: String(i).padStart(3, "0"), c: String(i) })
2222
+ )
2223
+
2224
+ await sandbox.seed(...items)
2225
+
2226
+ // Forwards
2227
+ const page = await C.paginate(
2228
+ { first: 10 },
2229
+ {
2230
+ KeyConditionExpression: "PK = :pk",
2231
+ ExpressionAttributeValues: { ":pk": "PK" }
2232
+ }
2233
+ )
2234
+ expect(page.pageInfo).toMatchInlineSnapshot(`
2235
+ Object {
2236
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6vfn9aOeA8=",
2237
+ "hasNextPage": true,
2238
+ "hasPreviousPage": false,
2239
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6v1n9aOeA8=",
2240
+ }
2241
+ `)
2242
+ expect(page.edges.length).toBe(10)
2243
+ expect(page.edges[0].node.c).toBe("0")
2244
+ expect(page.edges[9].node.c).toBe("9")
2245
+ })
2246
+
2247
+ test("it doesn't exceed the max limit", async () => {
2248
+ const items = Array.from({ length: 60 }).map(
2249
+ (_, i) =>
2250
+ new C({ pk: "PK", sk: String(i).padStart(3, "0"), c: String(i) })
2251
+ )
2252
+
2253
+ await sandbox.seed(...items)
2254
+
2255
+ // Forwards
2256
+ const page1 = await C.paginate(
2257
+ { first: 60 },
2258
+ {
2259
+ KeyConditionExpression: "PK = :pk",
2260
+ ExpressionAttributeValues: { ":pk": "PK" }
2261
+ }
2262
+ )
2263
+ expect(page1.pageInfo).toMatchInlineSnapshot(`
2264
+ Object {
2265
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOoKvfn9aOeA8=",
2266
+ "hasNextPage": true,
2267
+ "hasPreviousPage": false,
2268
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6v1n9aOeA8=",
2269
+ }
2270
+ `)
2271
+ expect(page1.edges.length).toBe(50)
2272
+ expect(page1.edges[0].node.c).toBe("0")
2273
+ expect(page1.edges[49].node.c).toBe("49")
2274
+ })
2275
+ })
2276
+
2277
+ describe("union", () => {
2278
+ test("it paginates a union model", async () => {
2279
+ const items = Array.from({ length: 60 }).map((_, i) =>
2280
+ i > 30
2281
+ ? new C({ pk: "PK", sk: String(i).padStart(3, "0"), c: String(i) })
2282
+ : new D({ pk: "PK", sk: String(i).padStart(3, "0"), d: String(i) })
2283
+ )
2284
+
2285
+ await sandbox.seed(...items)
2286
+
2287
+ // Forwards
2288
+ const page1 = await Union.paginate(
2289
+ {},
2290
+ {
2291
+ KeyConditionExpression: "PK = :pk",
2292
+ ExpressionAttributeValues: { ":pk": "PK" }
2293
+ }
2294
+ )
2295
+ expect(page1.pageInfo).toMatchInlineSnapshot(`
2296
+ Object {
2297
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo7vfn9aOeA8=",
2298
+ "hasNextPage": true,
2299
+ "hasPreviousPage": false,
2300
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6v1n9aOeA8=",
2301
+ }
2302
+ `)
2303
+ expect(page1.edges.length).toBe(20)
2304
+ expect(page1.edges[0].node.SK).toBe("000")
2305
+ expect(page1.edges[19].node.SK).toBe("019")
2306
+
2307
+ const page2 = await Union.paginate(
2308
+ { after: page1.pageInfo.endCursor },
2309
+ {
2310
+ KeyConditionExpression: "PK = :pk",
2311
+ ExpressionAttributeValues: { ":pk": "PK" }
2312
+ }
2313
+ )
2314
+ expect(page2.pageInfo).toMatchInlineSnapshot(`
2315
+ Object {
2316
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo5Xfn9aOeA8=",
2317
+ "hasNextPage": true,
2318
+ "hasPreviousPage": false,
2319
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo4X1n9aOeA8=",
2320
+ }
2321
+ `)
2322
+ expect(page2.edges.length).toBe(20)
2323
+ expect(page2.edges[0].node.SK).toBe("020")
2324
+ expect(page2.edges[19].node.SK).toBe("039")
2325
+
2326
+ const page3 = await Union.paginate(
2327
+ { after: page2.pageInfo.endCursor },
2328
+ {
2329
+ KeyConditionExpression: "PK = :pk",
2330
+ ExpressionAttributeValues: { ":pk": "PK" }
2331
+ }
2332
+ )
2333
+ expect(page3.pageInfo).toMatchInlineSnapshot(`
2334
+ Object {
2335
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOoLvfn9aOeA8=",
2336
+ "hasNextPage": false,
2337
+ "hasPreviousPage": false,
2338
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOoKv1n9aOeA8=",
2339
+ }
2340
+ `)
2341
+ expect(page3.edges.length).toBe(20)
2342
+ expect(page3.edges[0].node.SK).toBe("040")
2343
+ expect(page3.edges[19].node.SK).toBe("059")
2344
+
2345
+ // Backwards
2346
+ const backwardsPage2 = await Union.paginate(
2347
+ { before: page3.pageInfo.startCursor },
2348
+ {
2349
+ KeyConditionExpression: "PK = :pk",
2350
+ ExpressionAttributeValues: { ":pk": "PK" }
2351
+ }
2352
+ )
2353
+ expect(backwardsPage2.pageInfo).toMatchInlineSnapshot(`
2354
+ Object {
2355
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo5Xfn9aOeA8=",
2356
+ "hasNextPage": false,
2357
+ "hasPreviousPage": true,
2358
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo4X1n9aOeA8=",
2359
+ }
2360
+ `)
2361
+ expect(backwardsPage2.edges.length).toBe(20)
2362
+ expect(backwardsPage2.edges[0].node.SK).toBe("020")
2363
+ expect(backwardsPage2.edges[19].node.SK).toBe("039")
2364
+
2365
+ const backwardsPage1 = await Union.paginate(
2366
+ { before: backwardsPage2.pageInfo.startCursor },
2367
+ {
2368
+ KeyConditionExpression: "PK = :pk",
2369
+ ExpressionAttributeValues: { ":pk": "PK" }
2370
+ }
2371
+ )
2372
+ expect(backwardsPage1.pageInfo).toMatchInlineSnapshot(`
2373
+ Object {
2374
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo7vfn9aOeA8=",
2375
+ "hasNextPage": false,
2376
+ "hasPreviousPage": false,
2377
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6v1n9aOeA8=",
2378
+ }
2379
+ `)
2380
+ expect(backwardsPage1.edges.length).toBe(20)
2381
+ expect(backwardsPage1.edges[0].node.SK).toBe("000")
2382
+ expect(backwardsPage1.edges[19].node.SK).toBe("019")
2383
+ })
2384
+
2385
+ test("it respects a limit", async () => {
2386
+ const items = Array.from({ length: 60 }).map((_, i) =>
2387
+ i > 30
2388
+ ? new C({ pk: "PK", sk: String(i).padStart(3, "0"), c: String(i) })
2389
+ : new D({ pk: "PK", sk: String(i).padStart(3, "0"), d: String(i) })
2390
+ )
2391
+
2392
+ await sandbox.seed(...items)
2393
+
2394
+ // Forwards
2395
+ const page = await Union.paginate(
2396
+ { first: 10 },
2397
+ {
2398
+ KeyConditionExpression: "PK = :pk",
2399
+ ExpressionAttributeValues: { ":pk": "PK" }
2400
+ }
2401
+ )
2402
+ expect(page.pageInfo).toMatchInlineSnapshot(`
2403
+ Object {
2404
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6vfn9aOeA8=",
2405
+ "hasNextPage": true,
2406
+ "hasPreviousPage": false,
2407
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6v1n9aOeA8=",
2408
+ }
2409
+ `)
2410
+ expect(page.edges.length).toBe(10)
2411
+ expect(page.edges[0].node.SK).toBe("000")
2412
+ expect(page.edges[9].node.SK).toBe("009")
2413
+ })
2414
+
2415
+ test("it doesn't exceed the max limit", async () => {
2416
+ const items = Array.from({ length: 60 }).map((_, i) =>
2417
+ i > 30
2418
+ ? new C({ pk: "PK", sk: String(i).padStart(3, "0"), c: String(i) })
2419
+ : new D({ pk: "PK", sk: String(i).padStart(3, "0"), d: String(i) })
2420
+ )
2421
+
2422
+ await sandbox.seed(...items)
2423
+
2424
+ // Forwards
2425
+ const page1 = await Union.paginate(
2426
+ { first: 60 },
2427
+ {
2428
+ KeyConditionExpression: "PK = :pk",
2429
+ ExpressionAttributeValues: { ":pk": "PK" }
2430
+ }
2431
+ )
2432
+ expect(page1.pageInfo).toMatchInlineSnapshot(`
2433
+ Object {
2434
+ "endCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOoKvfn9aOeA8=",
2435
+ "hasNextPage": true,
2436
+ "hasPreviousPage": false,
2437
+ "startCursor": "cC4wNVXawu0oBvB8vqW4J/RG6hbr3ndOo6v1n9aOeA8=",
2438
+ }
2439
+ `)
2440
+ expect(page1.edges.length).toBe(50)
2441
+ expect(page1.edges[0].node.SK).toBe("000")
2442
+ expect(page1.edges[49].node.SK).toBe("049")
2443
+ })
2444
+ })
2445
+ })