@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
@@ -8,7 +8,7 @@ import {
8
8
  ItemNotFoundError,
9
9
  ConditionalCheckFailedError,
10
10
  RaceConditionError,
11
- BulkWriteTransactionError
11
+ BulkWriteTransactionError,
12
12
  } from "../errors"
13
13
 
14
14
  const client = new Client({ tableName: "table" })
@@ -16,7 +16,7 @@ const provider = getProvider(client)
16
16
 
17
17
  const SIMPLE_CODEC = t.type({
18
18
  foo: t.string,
19
- bar: t.number
19
+ bar: t.number,
20
20
  })
21
21
 
22
22
  class Simple extends model("Simple", SIMPLE_CODEC, provider) {
@@ -331,7 +331,7 @@ describe("put", () => {
331
331
 
332
332
  await expect(
333
333
  MultiGSI.put(new MultiGSI({ foo: "yes", bar: 42 }), {
334
- IgnoreExistence: true
334
+ IgnoreExistence: true,
335
335
  })
336
336
  ).resolves.toBeInstanceOf(MultiGSI)
337
337
  })
@@ -351,7 +351,7 @@ describe("get", () => {
351
351
 
352
352
  const result = await Simple.get({
353
353
  PK: item.keys().PK,
354
- SK: item.keys().SK
354
+ SK: item.keys().SK,
355
355
  })
356
356
 
357
357
  expect(result.values()).toMatchInlineSnapshot(`
@@ -417,8 +417,8 @@ describe("delete", () => {
417
417
  _model: Simple,
418
418
  key: {
419
419
  PK: item.keys().PK,
420
- SK: item.keys().SK
421
- }
420
+ SK: item.keys().SK,
421
+ },
422
422
  })
423
423
 
424
424
  expect(result).toBeNull()
@@ -451,7 +451,7 @@ describe("delete", () => {
451
451
 
452
452
  const result = await Simple.delete({
453
453
  PK: item.keys().PK,
454
- SK: item.keys().SK
454
+ SK: item.keys().SK,
455
455
  })
456
456
 
457
457
  expect(result).toBeNull()
@@ -826,7 +826,7 @@ describe("query", () => {
826
826
  await client.query(
827
827
  {
828
828
  KeyConditionExpression: `PK = :pk and begins_with(SK, :sk)`,
829
- ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT" }
829
+ ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT" },
830
830
  },
831
831
  { a: A, b: B, union: Union }
832
832
  )
@@ -850,7 +850,7 @@ describe("query", () => {
850
850
  await client.query(
851
851
  {
852
852
  KeyConditionExpression: `PK = :pk and begins_with(SK, :sk)`,
853
- ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT#" }
853
+ ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT#" },
854
854
  },
855
855
  { a: A, b: B, union: Union }
856
856
  )
@@ -886,7 +886,7 @@ describe("query", () => {
886
886
  const { a, b, union, _unknown, meta } = await client.query(
887
887
  {
888
888
  KeyConditionExpression: `PK = :pk and begins_with(SK, :sk)`,
889
- ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT#" }
889
+ ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT#" },
890
890
  },
891
891
  { a: A, b: B, union: Union }
892
892
  )
@@ -894,9 +894,9 @@ describe("query", () => {
894
894
  expect({
895
895
  meta: meta,
896
896
  _unknown: _unknown,
897
- a: a.map(item => item.values()),
898
- b: b.map(item => item.values()),
899
- union: union.map(item => item.values())
897
+ a: a.map((item) => item.values()),
898
+ b: b.map((item) => item.values()),
899
+ union: union.map((item) => item.values()),
900
900
  }).toMatchInlineSnapshot(`
901
901
  Object {
902
902
  "_unknown": Array [
@@ -959,7 +959,7 @@ describe("query", () => {
959
959
  {
960
960
  KeyConditionExpression: `PK = :pk and begins_with(SK, :sk)`,
961
961
  ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT#" },
962
- Limit: 30
962
+ Limit: 30,
963
963
  },
964
964
  { a: A, b: B }
965
965
  )
@@ -974,7 +974,7 @@ describe("query", () => {
974
974
  KeyConditionExpression: `PK = :pk and begins_with(SK, :sk)`,
975
975
  ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT#" },
976
976
  Limit: 30,
977
- ExclusiveStartKey: firstPage.meta.lastEvaluatedKey
977
+ ExclusiveStartKey: firstPage.meta.lastEvaluatedKey,
978
978
  },
979
979
  { a: A, b: B }
980
980
  )
@@ -1002,7 +1002,7 @@ describe("query", () => {
1002
1002
  ExpressionAttributeValues: { ":pk": "abc", ":sk": "SORT#" },
1003
1003
  FetchAllPages: true,
1004
1004
  // You wouldn't set a limit in a real-world use case here to optimize fetching all items.
1005
- Limit: 10
1005
+ Limit: 10,
1006
1006
  },
1007
1007
  { a: A, b: B }
1008
1008
  )
@@ -1039,16 +1039,16 @@ describe("bulk", () => {
1039
1039
  new B({
1040
1040
  pk: "PK#UPDATE",
1041
1041
  sk: "SK#UPDATE",
1042
- b: "bar"
1042
+ b: "bar",
1043
1043
  }).operation("update", { b: "baz" }),
1044
1044
  new B({
1045
1045
  pk: "PK#COND",
1046
1046
  sk: "SK#COND",
1047
- b: "cond"
1047
+ b: "cond",
1048
1048
  }).operation("condition", {
1049
1049
  ConditionExpression: "b = :cond",
1050
- ExpressionAttributeValues: { ":cond": "cond" }
1051
- })
1050
+ ExpressionAttributeValues: { ":cond": "cond" },
1051
+ }),
1052
1052
  ])
1053
1053
 
1054
1054
  expect(await sandbox.diff(before)).toMatchInlineSnapshot(`
@@ -1168,7 +1168,7 @@ describe("bulk", () => {
1168
1168
  "updateRaw",
1169
1169
  { PK: "PK#nicetry", SK: "SK#nope" },
1170
1170
  { a: 234 }
1171
- )
1171
+ ),
1172
1172
  ])
1173
1173
  ).rejects.toBeInstanceOf(BulkWriteTransactionError)
1174
1174
 
@@ -1196,11 +1196,11 @@ describe("bulk", () => {
1196
1196
  new B({
1197
1197
  pk: "PK#UPDATE",
1198
1198
  sk: "SK#UPDATE",
1199
- b: "bar"
1199
+ b: "bar",
1200
1200
  }).operation("update", { b: "baz" }),
1201
1201
  ...Array.from({ length: 25 }).map((_, i) =>
1202
1202
  new A({ pk: `PK#A${i}`, sk: `SK#A${i}`, a: i }).operation("put")
1203
- )
1203
+ ),
1204
1204
  ])
1205
1205
 
1206
1206
  //#region snapshot
@@ -1512,7 +1512,7 @@ describe("bulk", () => {
1512
1512
  "condition",
1513
1513
  { PK: "nicetry", SK: "nope" },
1514
1514
  { ConditionExpression: "attribute_exists(PK)" }
1515
- )
1515
+ ),
1516
1516
  ])
1517
1517
  ).rejects.toBeInstanceOf(BulkWriteTransactionError)
1518
1518
 
@@ -1546,7 +1546,7 @@ describe("batchGet", () => {
1546
1546
  two: A.operation("get", { PK: "PK#2", SK: "SK#2" }),
1547
1547
  three: A.operation("get", { PK: "PK#3", SK: "SK#3" }),
1548
1548
  four: A.operation("get", { PK: "PK#4", SK: "SK#4" }),
1549
- duplicate: A.operation("get", { PK: "PK#1", SK: "SK#1" })
1549
+ duplicate: A.operation("get", { PK: "PK#1", SK: "SK#1" }),
1550
1550
  })
1551
1551
  ).rejects.toBeInstanceOf(ItemNotFoundError)
1552
1552
  })
@@ -1563,7 +1563,7 @@ describe("batchGet", () => {
1563
1563
  two: A.operation("get", { PK: "PK#2", SK: "SK#2" }),
1564
1564
  duplicate: A.operation("get", { PK: "PK#1", SK: "SK#1" }),
1565
1565
  error: A.operation("get", { PK: "PK#error", SK: "SK#error" }),
1566
- error2: A.operation("get", { PK: "PK#error2", SK: "SK#error2" })
1566
+ error2: A.operation("get", { PK: "PK#error2", SK: "SK#error2" }),
1567
1567
  },
1568
1568
  { individualErrors: true }
1569
1569
  )
@@ -1587,7 +1587,7 @@ describe("batchGet", () => {
1587
1587
  two: A.operation("get", { PK: "PK#2", SK: "SK#2" }),
1588
1588
  three: A.operation("get", { PK: "PK#3", SK: "SK#3" }),
1589
1589
  four: A.operation("get", { PK: "PK#4", SK: "SK#4" }),
1590
- duplicate: A.operation("get", { PK: "PK#1", SK: "SK#1" })
1590
+ duplicate: A.operation("get", { PK: "PK#1", SK: "SK#1" }),
1591
1591
  })
1592
1592
 
1593
1593
  expect(
@@ -1640,6 +1640,41 @@ describe("load", () => {
1640
1640
  ).resolves.toBeNull()
1641
1641
  })
1642
1642
 
1643
+ test("it recovers a soft deleted item", async () => {
1644
+ const item = new A({ pk: "PK#1", sk: "SK#1", a: 1 })
1645
+
1646
+ await sandbox.seed(item)
1647
+
1648
+ await item.softDelete()
1649
+
1650
+ const recovered = await client.load(
1651
+ A.operation("get", { PK: "PK#1", SK: "SK#1" }),
1652
+ {
1653
+ recover: true,
1654
+ }
1655
+ )
1656
+
1657
+ expect(recovered).toBeInstanceOf(A)
1658
+ expect(recovered.isDeleted).toBe(true)
1659
+ })
1660
+
1661
+ test("it throws if no item or soft deleted item exists", async () => {
1662
+ await expect(
1663
+ client.load(A.operation("get", { PK: "PK", SK: "SK" }), {
1664
+ recover: true,
1665
+ })
1666
+ ).rejects.toBeInstanceOf(ItemNotFoundError)
1667
+ })
1668
+
1669
+ test("it returns null instead of throwing if no item or soft deleted item exists", async () => {
1670
+ await expect(
1671
+ client.load(A.operation("get", { PK: "PK", SK: "SK" }), {
1672
+ recover: true,
1673
+ null: true,
1674
+ })
1675
+ ).resolves.toBeNull()
1676
+ })
1677
+
1643
1678
  test("it fetches >100 items", async () => {
1644
1679
  const items = Array.from({ length: 234 }).map((_, i) =>
1645
1680
  i < 100
@@ -1730,8 +1765,8 @@ describe("load", () => {
1730
1765
  )
1731
1766
 
1732
1767
  expect(results.length).toBe(234)
1733
- expect(results.filter(item => item instanceof C).length).toBe(123)
1734
- expect(results.filter(item => item instanceof D).length).toBe(111)
1768
+ expect(results.filter((item) => item instanceof C).length).toBe(123)
1769
+ expect(results.filter((item) => item instanceof D).length).toBe(111)
1735
1770
  expect(spy).toHaveBeenCalledTimes(3)
1736
1771
 
1737
1772
  spy.mockReset()
@@ -1806,8 +1841,8 @@ describe("loadMany", () => {
1806
1841
  )
1807
1842
 
1808
1843
  expect(results.length).toBe(234)
1809
- expect(results.filter(item => item instanceof C).length).toBe(123)
1810
- expect(results.filter(item => item instanceof D).length).toBe(111)
1844
+ expect(results.filter((item) => item instanceof C).length).toBe(123)
1845
+ expect(results.filter((item) => item instanceof D).length).toBe(111)
1811
1846
  expect(spy).toHaveBeenCalledTimes(3)
1812
1847
 
1813
1848
  spy.mockReset()
@@ -1832,7 +1867,7 @@ describe("paginate", () => {
1832
1867
  {},
1833
1868
  {
1834
1869
  KeyConditionExpression: "PK = :pk",
1835
- ExpressionAttributeValues: { ":pk": "PK" }
1870
+ ExpressionAttributeValues: { ":pk": "PK" },
1836
1871
  }
1837
1872
  )
1838
1873
  expect(page1.pageInfo).toMatchInlineSnapshot(`
@@ -1852,7 +1887,7 @@ describe("paginate", () => {
1852
1887
  { after: page1.pageInfo.endCursor },
1853
1888
  {
1854
1889
  KeyConditionExpression: "PK = :pk",
1855
- ExpressionAttributeValues: { ":pk": "PK" }
1890
+ ExpressionAttributeValues: { ":pk": "PK" },
1856
1891
  }
1857
1892
  )
1858
1893
  expect(page2.pageInfo).toMatchInlineSnapshot(`
@@ -1872,7 +1907,7 @@ describe("paginate", () => {
1872
1907
  { after: page2.pageInfo.endCursor },
1873
1908
  {
1874
1909
  KeyConditionExpression: "PK = :pk",
1875
- ExpressionAttributeValues: { ":pk": "PK" }
1910
+ ExpressionAttributeValues: { ":pk": "PK" },
1876
1911
  }
1877
1912
  )
1878
1913
  expect(page3.pageInfo).toMatchInlineSnapshot(`
@@ -1893,7 +1928,7 @@ describe("paginate", () => {
1893
1928
  { before: page3.pageInfo.startCursor },
1894
1929
  {
1895
1930
  KeyConditionExpression: "PK = :pk",
1896
- ExpressionAttributeValues: { ":pk": "PK" }
1931
+ ExpressionAttributeValues: { ":pk": "PK" },
1897
1932
  }
1898
1933
  )
1899
1934
  expect(backwardsPage2.pageInfo).toMatchInlineSnapshot(`
@@ -1913,7 +1948,7 @@ describe("paginate", () => {
1913
1948
  { before: backwardsPage2.pageInfo.startCursor },
1914
1949
  {
1915
1950
  KeyConditionExpression: "PK = :pk",
1916
- ExpressionAttributeValues: { ":pk": "PK" }
1951
+ ExpressionAttributeValues: { ":pk": "PK" },
1917
1952
  }
1918
1953
  )
1919
1954
  expect(backwardsPage1.pageInfo).toMatchInlineSnapshot(`
@@ -1944,7 +1979,7 @@ describe("paginate", () => {
1944
1979
  {},
1945
1980
  {
1946
1981
  KeyConditionExpression: "PK = :pk",
1947
- ExpressionAttributeValues: { ":pk": "PK" }
1982
+ ExpressionAttributeValues: { ":pk": "PK" },
1948
1983
  }
1949
1984
  )
1950
1985
  expect(page1.pageInfo).toMatchInlineSnapshot(`
@@ -1964,7 +1999,7 @@ describe("paginate", () => {
1964
1999
  { after: page1.pageInfo.endCursor },
1965
2000
  {
1966
2001
  KeyConditionExpression: "PK = :pk",
1967
- ExpressionAttributeValues: { ":pk": "PK" }
2002
+ ExpressionAttributeValues: { ":pk": "PK" },
1968
2003
  }
1969
2004
  )
1970
2005
  expect(page2.pageInfo).toMatchInlineSnapshot(`
@@ -1984,7 +2019,7 @@ describe("paginate", () => {
1984
2019
  { after: page2.pageInfo.endCursor },
1985
2020
  {
1986
2021
  KeyConditionExpression: "PK = :pk",
1987
- ExpressionAttributeValues: { ":pk": "PK" }
2022
+ ExpressionAttributeValues: { ":pk": "PK" },
1988
2023
  }
1989
2024
  )
1990
2025
  expect(page3.pageInfo).toMatchInlineSnapshot(`
@@ -2005,7 +2040,7 @@ describe("paginate", () => {
2005
2040
  { before: page3.pageInfo.startCursor },
2006
2041
  {
2007
2042
  KeyConditionExpression: "PK = :pk",
2008
- ExpressionAttributeValues: { ":pk": "PK" }
2043
+ ExpressionAttributeValues: { ":pk": "PK" },
2009
2044
  }
2010
2045
  )
2011
2046
  expect(backwardsPage2.pageInfo).toMatchInlineSnapshot(`
@@ -2025,7 +2060,7 @@ describe("paginate", () => {
2025
2060
  { before: backwardsPage2.pageInfo.startCursor },
2026
2061
  {
2027
2062
  KeyConditionExpression: "PK = :pk",
2028
- ExpressionAttributeValues: { ":pk": "PK" }
2063
+ ExpressionAttributeValues: { ":pk": "PK" },
2029
2064
  }
2030
2065
  )
2031
2066
  expect(backwardsPage1.pageInfo).toMatchInlineSnapshot(`
@@ -2055,7 +2090,7 @@ describe("paginate", () => {
2055
2090
  { first: 10 },
2056
2091
  {
2057
2092
  KeyConditionExpression: "PK = :pk",
2058
- ExpressionAttributeValues: { ":pk": "PK" }
2093
+ ExpressionAttributeValues: { ":pk": "PK" },
2059
2094
  }
2060
2095
  )
2061
2096
  expect(page.pageInfo).toMatchInlineSnapshot(`
@@ -2085,7 +2120,7 @@ describe("paginate", () => {
2085
2120
  { first: 60 },
2086
2121
  {
2087
2122
  KeyConditionExpression: "PK = :pk",
2088
- ExpressionAttributeValues: { ":pk": "PK" }
2123
+ ExpressionAttributeValues: { ":pk": "PK" },
2089
2124
  }
2090
2125
  )
2091
2126
  expect(page1.pageInfo).toMatchInlineSnapshot(`
@@ -2116,7 +2151,7 @@ describe("paginate", () => {
2116
2151
  {},
2117
2152
  {
2118
2153
  KeyConditionExpression: "PK = :pk",
2119
- ExpressionAttributeValues: { ":pk": "PK" }
2154
+ ExpressionAttributeValues: { ":pk": "PK" },
2120
2155
  }
2121
2156
  )
2122
2157
  expect(page1.pageInfo).toMatchInlineSnapshot(`
@@ -2135,7 +2170,7 @@ describe("paginate", () => {
2135
2170
  { after: page1.pageInfo.endCursor },
2136
2171
  {
2137
2172
  KeyConditionExpression: "PK = :pk",
2138
- ExpressionAttributeValues: { ":pk": "PK" }
2173
+ ExpressionAttributeValues: { ":pk": "PK" },
2139
2174
  }
2140
2175
  )
2141
2176
  expect(page2.pageInfo).toMatchInlineSnapshot(`
@@ -2154,7 +2189,7 @@ describe("paginate", () => {
2154
2189
  { after: page2.pageInfo.endCursor },
2155
2190
  {
2156
2191
  KeyConditionExpression: "PK = :pk",
2157
- ExpressionAttributeValues: { ":pk": "PK" }
2192
+ ExpressionAttributeValues: { ":pk": "PK" },
2158
2193
  }
2159
2194
  )
2160
2195
  expect(page3.pageInfo).toMatchInlineSnapshot(`
@@ -2174,7 +2209,7 @@ describe("paginate", () => {
2174
2209
  { before: page3.pageInfo.startCursor },
2175
2210
  {
2176
2211
  KeyConditionExpression: "PK = :pk",
2177
- ExpressionAttributeValues: { ":pk": "PK" }
2212
+ ExpressionAttributeValues: { ":pk": "PK" },
2178
2213
  }
2179
2214
  )
2180
2215
  expect(backwardsPage2.pageInfo).toMatchInlineSnapshot(`
@@ -2193,7 +2228,7 @@ describe("paginate", () => {
2193
2228
  { before: backwardsPage2.pageInfo.startCursor },
2194
2229
  {
2195
2230
  KeyConditionExpression: "PK = :pk",
2196
- ExpressionAttributeValues: { ":pk": "PK" }
2231
+ ExpressionAttributeValues: { ":pk": "PK" },
2197
2232
  }
2198
2233
  )
2199
2234
  expect(backwardsPage1.pageInfo).toMatchInlineSnapshot(`
@@ -2222,7 +2257,7 @@ describe("paginate", () => {
2222
2257
  { first: 10 },
2223
2258
  {
2224
2259
  KeyConditionExpression: "PK = :pk",
2225
- ExpressionAttributeValues: { ":pk": "PK" }
2260
+ ExpressionAttributeValues: { ":pk": "PK" },
2226
2261
  }
2227
2262
  )
2228
2263
  expect(page.pageInfo).toMatchInlineSnapshot(`
@@ -2251,7 +2286,7 @@ describe("paginate", () => {
2251
2286
  { first: 60 },
2252
2287
  {
2253
2288
  KeyConditionExpression: "PK = :pk",
2254
- ExpressionAttributeValues: { ":pk": "PK" }
2289
+ ExpressionAttributeValues: { ":pk": "PK" },
2255
2290
  }
2256
2291
  )
2257
2292
  expect(page1.pageInfo).toMatchInlineSnapshot(`
@@ -2283,7 +2318,7 @@ describe("paginate", () => {
2283
2318
  {},
2284
2319
  {
2285
2320
  KeyConditionExpression: "PK = :pk",
2286
- ExpressionAttributeValues: { ":pk": "PK" }
2321
+ ExpressionAttributeValues: { ":pk": "PK" },
2287
2322
  }
2288
2323
  )
2289
2324
  expect(page1.pageInfo).toMatchInlineSnapshot(`
@@ -2302,7 +2337,7 @@ describe("paginate", () => {
2302
2337
  { after: page1.pageInfo.endCursor },
2303
2338
  {
2304
2339
  KeyConditionExpression: "PK = :pk",
2305
- ExpressionAttributeValues: { ":pk": "PK" }
2340
+ ExpressionAttributeValues: { ":pk": "PK" },
2306
2341
  }
2307
2342
  )
2308
2343
  expect(page2.pageInfo).toMatchInlineSnapshot(`
@@ -2321,7 +2356,7 @@ describe("paginate", () => {
2321
2356
  { after: page2.pageInfo.endCursor },
2322
2357
  {
2323
2358
  KeyConditionExpression: "PK = :pk",
2324
- ExpressionAttributeValues: { ":pk": "PK" }
2359
+ ExpressionAttributeValues: { ":pk": "PK" },
2325
2360
  }
2326
2361
  )
2327
2362
  expect(page3.pageInfo).toMatchInlineSnapshot(`
@@ -2341,7 +2376,7 @@ describe("paginate", () => {
2341
2376
  { before: page3.pageInfo.startCursor },
2342
2377
  {
2343
2378
  KeyConditionExpression: "PK = :pk",
2344
- ExpressionAttributeValues: { ":pk": "PK" }
2379
+ ExpressionAttributeValues: { ":pk": "PK" },
2345
2380
  }
2346
2381
  )
2347
2382
  expect(backwardsPage2.pageInfo).toMatchInlineSnapshot(`
@@ -2360,7 +2395,7 @@ describe("paginate", () => {
2360
2395
  { before: backwardsPage2.pageInfo.startCursor },
2361
2396
  {
2362
2397
  KeyConditionExpression: "PK = :pk",
2363
- ExpressionAttributeValues: { ":pk": "PK" }
2398
+ ExpressionAttributeValues: { ":pk": "PK" },
2364
2399
  }
2365
2400
  )
2366
2401
  expect(backwardsPage1.pageInfo).toMatchInlineSnapshot(`
@@ -2390,7 +2425,7 @@ describe("paginate", () => {
2390
2425
  { first: 10 },
2391
2426
  {
2392
2427
  KeyConditionExpression: "PK = :pk",
2393
- ExpressionAttributeValues: { ":pk": "PK" }
2428
+ ExpressionAttributeValues: { ":pk": "PK" },
2394
2429
  }
2395
2430
  )
2396
2431
  expect(page.pageInfo).toMatchInlineSnapshot(`
@@ -2420,7 +2455,7 @@ describe("paginate", () => {
2420
2455
  { first: 60 },
2421
2456
  {
2422
2457
  KeyConditionExpression: "PK = :pk",
2423
- ExpressionAttributeValues: { ":pk": "PK" }
2458
+ ExpressionAttributeValues: { ":pk": "PK" },
2424
2459
  }
2425
2460
  )
2426
2461
  expect(page1.pageInfo).toMatchInlineSnapshot(`
package/src/client.ts CHANGED
@@ -79,6 +79,12 @@ export interface ClientProps
79
79
  ServiceConfigurationOptions,
80
80
  ClientApiVersions {
81
81
  tableName: string
82
+
83
+ /**
84
+ * The encryption key used to encrypt cursors using AES-256-CTR.
85
+ * Must be a 32 character string, 256 bits.
86
+ */
87
+ cursorEncryptionKey?: Buffer
82
88
  }
83
89
 
84
90
  export interface Key {
@@ -90,9 +96,11 @@ export class Client {
90
96
  tableName: string
91
97
  documentClient: DocumentClient
92
98
  dataLoader: DataLoader<GetOperation<Decodable>, DynamoDBModelInstance, string>
99
+ cursorEncryptionKey?: Buffer
93
100
 
94
101
  constructor(props: ClientProps) {
95
102
  this.tableName = props?.tableName
103
+ this.cursorEncryptionKey = props?.cursorEncryptionKey
96
104
  this.documentClient = new DocumentClient(props)
97
105
  this.dataLoader = new DataLoader<
98
106
  GetOperation<Decodable>,
@@ -189,7 +197,7 @@ export class Client {
189
197
  const { Item } = await this.documentClient
190
198
  .get({
191
199
  TableName: this.tableName,
192
- Key: key,
200
+ Key: { PK: key.PK, SK: key.SK },
193
201
  ...params,
194
202
  })
195
203
  .promise()
@@ -199,15 +207,45 @@ export class Client {
199
207
  return (_model as M & DynamoDBInternals<M>).__dynamoDBDecode(Item) as any
200
208
  }
201
209
 
202
- async load<M extends Decodable, Null extends boolean = false>(
210
+ async load<
211
+ M extends Decodable,
212
+ Null extends boolean = false,
213
+ Recover extends boolean = false
214
+ >(
203
215
  operation: GetOperation<M>,
204
- params?: { null?: Null }
216
+ params?: { null?: Null; recover?: Recover }
205
217
  ): Promise<
206
- Null extends true ? DecodableInstance<M> | null : DecodableInstance<M>
218
+ Recover extends true
219
+ ? Null extends true
220
+ ? (DecodableInstance<M> & { isDeleted?: true }) | null
221
+ : DecodableInstance<M> & { isDeleted?: true }
222
+ : Null extends true
223
+ ? DecodableInstance<M> | null
224
+ : DecodableInstance<M>
207
225
  > {
208
226
  const item = await this.dataLoader.load(operation).catch((e) => {
209
227
  // Maybe return null instead of throwing
210
- if (e instanceof ItemNotFoundError && params?.null) return null
228
+ if (e instanceof ItemNotFoundError) {
229
+ // Try to recover a deleted item
230
+ if (params?.recover)
231
+ return this.dataLoader
232
+ .load({
233
+ ...operation,
234
+ key: {
235
+ PK: `$$DELETED$$${operation.key.PK}`,
236
+ SK: `$$DELETED$$${operation.key.SK}`,
237
+ },
238
+ })
239
+ .then((item) => Object.assign(item, { isDeleted: true }))
240
+ .catch((e2) => {
241
+ if (e2 instanceof ItemNotFoundError && params.null) return null
242
+
243
+ // Throw the original error
244
+ throw e
245
+ })
246
+
247
+ if (params?.null) return null
248
+ }
211
249
 
212
250
  throw e
213
251
  })
@@ -494,7 +532,16 @@ export class Client {
494
532
  ...params,
495
533
  // Fetch one additional item to test for a next page
496
534
  Limit: limit + 1,
497
- ExclusiveStartKey: cursor ? decodeDDBCursor(cursor) : undefined,
535
+ ExclusiveStartKey: cursor
536
+ ? decodeDDBCursor(
537
+ cursor,
538
+ // GSI1 is the inverse index and uses PK and SK (switched around)
539
+ params.IndexName && params.IndexName !== "GSI1"
540
+ ? (params.IndexName as "GSI2" | "GSI3" | "GSI4" | "GSI5")
541
+ : undefined,
542
+ this.cursorEncryptionKey
543
+ )
544
+ : undefined,
498
545
  ScanIndexForward: direction === PaginationDirection.FORWARD,
499
546
  },
500
547
  { results: model }
@@ -509,13 +556,7 @@ export class Client {
509
556
  // Build edges
510
557
  const edges = slice.map((item: any) => ({
511
558
  node: item,
512
- cursor: encodeDDBCursor(
513
- item,
514
- // GSI1 is the inverse index and uses PK and SK (switched around)
515
- params.IndexName && params.IndexName !== "GSI1"
516
- ? (params.IndexName as "GSI2" | "GSI3")
517
- : undefined
518
- ),
559
+ cursor: encodeDDBCursor(item, this.cursorEncryptionKey),
519
560
  }))
520
561
 
521
562
  return {
@@ -11,6 +11,10 @@ export interface DynamoDBModelInstance extends ModelInstance<string, any> {
11
11
  GSI2SK?: string
12
12
  GSI3PK?: string
13
13
  GSI3SK?: string
14
+ GSI4PK?: string
15
+ GSI4SK?: string
16
+ GSI5PK?: string
17
+ GSI5SK?: string
14
18
  }
15
19
 
16
20
  PK: string