@model-ts/dynamodb 2.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/cjs/__test__/client-with-cursor-encryption.test.js +298 -183
- package/dist/cjs/__test__/client-with-cursor-encryption.test.js.map +1 -1
- package/dist/cjs/__test__/client.test.js +101 -0
- package/dist/cjs/__test__/client.test.js.map +1 -1
- package/dist/cjs/__test__/pagination.test.d.ts +1 -0
- package/dist/cjs/__test__/pagination.test.js +241 -0
- package/dist/cjs/__test__/pagination.test.js.map +1 -0
- package/dist/cjs/client.d.ts +6 -1
- package/dist/cjs/client.js +7 -2
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/dynamodb-model.d.ts +33 -8
- package/dist/cjs/gsi.d.ts +4 -0
- package/dist/cjs/gsi.js +24 -0
- package/dist/cjs/gsi.js.map +1 -0
- package/dist/cjs/operations.d.ts +4 -8
- package/dist/cjs/operations.js.map +1 -1
- package/dist/cjs/pagination.d.ts +48 -59
- package/dist/cjs/pagination.js +16 -26
- package/dist/cjs/pagination.js.map +1 -1
- package/dist/cjs/provider.d.ts +135 -17
- package/dist/cjs/provider.js +5 -12
- package/dist/cjs/provider.js.map +1 -1
- package/dist/cjs/sandbox.js +10 -43
- package/dist/cjs/sandbox.js.map +1 -1
- package/dist/esm/__test__/client-with-cursor-encryption.test.js +298 -183
- package/dist/esm/__test__/client-with-cursor-encryption.test.js.map +1 -1
- package/dist/esm/__test__/client.test.js +101 -0
- package/dist/esm/__test__/client.test.js.map +1 -1
- package/dist/esm/__test__/pagination.test.d.ts +1 -0
- package/dist/esm/__test__/pagination.test.js +238 -0
- package/dist/esm/__test__/pagination.test.js.map +1 -0
- package/dist/esm/client.d.ts +6 -1
- package/dist/esm/client.js +7 -2
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/dynamodb-model.d.ts +33 -8
- package/dist/esm/gsi.d.ts +4 -0
- package/dist/esm/gsi.js +21 -0
- package/dist/esm/gsi.js.map +1 -0
- package/dist/esm/operations.d.ts +4 -8
- package/dist/esm/operations.js.map +1 -1
- package/dist/esm/pagination.d.ts +48 -59
- package/dist/esm/pagination.js +17 -26
- package/dist/esm/pagination.js.map +1 -1
- package/dist/esm/provider.d.ts +135 -17
- package/dist/esm/provider.js +5 -12
- package/dist/esm/provider.js.map +1 -1
- package/dist/esm/sandbox.js +10 -43
- package/dist/esm/sandbox.js.map +1 -1
- package/package.json +1 -1
- package/src/__test__/client-with-cursor-encryption.test.ts +365 -183
- package/src/__test__/client.test.ts +168 -0
- package/src/__test__/pagination.test.ts +300 -0
- package/src/client.ts +20 -19
- package/src/dynamodb-model.ts +31 -9
- package/src/gsi.ts +25 -0
- package/src/operations.ts +4 -10
- package/src/pagination.ts +39 -52
- package/src/provider.ts +9 -9
- package/src/sandbox.ts +10 -43
- package/tsconfig.esm.json +14 -4
- package/tsconfig.json +14 -4
|
@@ -134,6 +134,10 @@ beforeEach(async () => {
|
|
|
134
134
|
sandbox = await createSandbox(client)
|
|
135
135
|
})
|
|
136
136
|
|
|
137
|
+
afterEach(async () => {
|
|
138
|
+
await sandbox.destroy()
|
|
139
|
+
})
|
|
140
|
+
|
|
137
141
|
describe("put", () => {
|
|
138
142
|
describe("via instance", () => {
|
|
139
143
|
test("it inserts a simple model", async () => {
|
|
@@ -2135,6 +2139,56 @@ describe("paginate", () => {
|
|
|
2135
2139
|
expect(page1.edges[0].node.c).toBe("0")
|
|
2136
2140
|
expect(page1.edges[49].node.c).toBe("49")
|
|
2137
2141
|
})
|
|
2142
|
+
|
|
2143
|
+
test("it respects custom pagination default", async () => {
|
|
2144
|
+
client.paginationOptions = {
|
|
2145
|
+
default: 40,
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
const items = Array.from({ length: 50 }).map(
|
|
2149
|
+
(_, i) =>
|
|
2150
|
+
new C({ pk: "PK", sk: String(i).padStart(3, "0"), c: String(i) })
|
|
2151
|
+
)
|
|
2152
|
+
|
|
2153
|
+
await sandbox.seed(...items)
|
|
2154
|
+
|
|
2155
|
+
const page = await client.paginate(
|
|
2156
|
+
C,
|
|
2157
|
+
{},
|
|
2158
|
+
{
|
|
2159
|
+
KeyConditionExpression: "PK = :pk",
|
|
2160
|
+
ExpressionAttributeValues: { ":pk": "PK" },
|
|
2161
|
+
}
|
|
2162
|
+
)
|
|
2163
|
+
expect(page.edges.length).toBe(40)
|
|
2164
|
+
|
|
2165
|
+
delete client.paginationOptions
|
|
2166
|
+
})
|
|
2167
|
+
|
|
2168
|
+
test("it respects custom pagination limit", async () => {
|
|
2169
|
+
client.paginationOptions = {
|
|
2170
|
+
limit: 100,
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
const items = Array.from({ length: 120 }).map(
|
|
2174
|
+
(_, i) =>
|
|
2175
|
+
new C({ pk: "PK", sk: String(i).padStart(3, "0"), c: String(i) })
|
|
2176
|
+
)
|
|
2177
|
+
|
|
2178
|
+
await sandbox.seed(...items)
|
|
2179
|
+
|
|
2180
|
+
const page = await client.paginate(
|
|
2181
|
+
C,
|
|
2182
|
+
{ first: 110 },
|
|
2183
|
+
{
|
|
2184
|
+
KeyConditionExpression: "PK = :pk",
|
|
2185
|
+
ExpressionAttributeValues: { ":pk": "PK" },
|
|
2186
|
+
}
|
|
2187
|
+
)
|
|
2188
|
+
expect(page.edges.length).toBe(100)
|
|
2189
|
+
|
|
2190
|
+
delete client.paginationOptions
|
|
2191
|
+
})
|
|
2138
2192
|
})
|
|
2139
2193
|
|
|
2140
2194
|
describe("model", () => {
|
|
@@ -2301,6 +2355,54 @@ describe("paginate", () => {
|
|
|
2301
2355
|
expect(page1.edges[0].node.c).toBe("0")
|
|
2302
2356
|
expect(page1.edges[49].node.c).toBe("49")
|
|
2303
2357
|
})
|
|
2358
|
+
|
|
2359
|
+
test("it respects custom pagination default", async () => {
|
|
2360
|
+
client.paginationOptions = {
|
|
2361
|
+
default: 40,
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
const items = Array.from({ length: 50 }).map(
|
|
2365
|
+
(_, i) =>
|
|
2366
|
+
new C({ pk: "PK", sk: String(i).padStart(3, "0"), c: String(i) })
|
|
2367
|
+
)
|
|
2368
|
+
|
|
2369
|
+
await sandbox.seed(...items)
|
|
2370
|
+
|
|
2371
|
+
const page = await C.paginate(
|
|
2372
|
+
{},
|
|
2373
|
+
{
|
|
2374
|
+
KeyConditionExpression: "PK = :pk",
|
|
2375
|
+
ExpressionAttributeValues: { ":pk": "PK" },
|
|
2376
|
+
}
|
|
2377
|
+
)
|
|
2378
|
+
expect(page.edges.length).toBe(40)
|
|
2379
|
+
|
|
2380
|
+
delete client.paginationOptions
|
|
2381
|
+
})
|
|
2382
|
+
|
|
2383
|
+
test("it respects custom pagination limit", async () => {
|
|
2384
|
+
client.paginationOptions = {
|
|
2385
|
+
limit: 100,
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
const items = Array.from({ length: 120 }).map(
|
|
2389
|
+
(_, i) =>
|
|
2390
|
+
new C({ pk: "PK", sk: String(i).padStart(3, "0"), c: String(i) })
|
|
2391
|
+
)
|
|
2392
|
+
|
|
2393
|
+
await sandbox.seed(...items)
|
|
2394
|
+
|
|
2395
|
+
const page = await C.paginate(
|
|
2396
|
+
{ first: 110 },
|
|
2397
|
+
{
|
|
2398
|
+
KeyConditionExpression: "PK = :pk",
|
|
2399
|
+
ExpressionAttributeValues: { ":pk": "PK" },
|
|
2400
|
+
}
|
|
2401
|
+
)
|
|
2402
|
+
expect(page.edges.length).toBe(100)
|
|
2403
|
+
|
|
2404
|
+
delete client.paginationOptions
|
|
2405
|
+
})
|
|
2304
2406
|
})
|
|
2305
2407
|
|
|
2306
2408
|
describe("union", () => {
|
|
@@ -2470,5 +2572,71 @@ describe("paginate", () => {
|
|
|
2470
2572
|
expect(page1.edges[0].node.SK).toBe("000")
|
|
2471
2573
|
expect(page1.edges[49].node.SK).toBe("049")
|
|
2472
2574
|
})
|
|
2575
|
+
|
|
2576
|
+
test("it respects custom pagination default", async () => {
|
|
2577
|
+
client.paginationOptions = {
|
|
2578
|
+
default: 40,
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
const items = Array.from({ length: 50 }).map((_, i) =>
|
|
2582
|
+
i > 30
|
|
2583
|
+
? new C({
|
|
2584
|
+
pk: "PK",
|
|
2585
|
+
sk: String(i).padStart(3, "0"),
|
|
2586
|
+
c: String(i),
|
|
2587
|
+
})
|
|
2588
|
+
: new D({
|
|
2589
|
+
pk: "PK",
|
|
2590
|
+
sk: String(i).padStart(3, "0"),
|
|
2591
|
+
d: String(i),
|
|
2592
|
+
})
|
|
2593
|
+
)
|
|
2594
|
+
|
|
2595
|
+
await sandbox.seed(...items)
|
|
2596
|
+
|
|
2597
|
+
const page = await Union.paginate(
|
|
2598
|
+
{},
|
|
2599
|
+
{
|
|
2600
|
+
KeyConditionExpression: "PK = :pk",
|
|
2601
|
+
ExpressionAttributeValues: { ":pk": "PK" },
|
|
2602
|
+
}
|
|
2603
|
+
)
|
|
2604
|
+
expect(page.edges.length).toBe(40)
|
|
2605
|
+
|
|
2606
|
+
delete client.paginationOptions
|
|
2607
|
+
})
|
|
2608
|
+
|
|
2609
|
+
test("it respects custom pagination limit", async () => {
|
|
2610
|
+
client.paginationOptions = {
|
|
2611
|
+
limit: 100,
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
const items = Array.from({ length: 110 }).map((_, i) =>
|
|
2615
|
+
i > 30
|
|
2616
|
+
? new C({
|
|
2617
|
+
pk: "PK",
|
|
2618
|
+
sk: String(i).padStart(3, "0"),
|
|
2619
|
+
c: String(i),
|
|
2620
|
+
})
|
|
2621
|
+
: new D({
|
|
2622
|
+
pk: "PK",
|
|
2623
|
+
sk: String(i).padStart(3, "0"),
|
|
2624
|
+
d: String(i),
|
|
2625
|
+
})
|
|
2626
|
+
)
|
|
2627
|
+
|
|
2628
|
+
await sandbox.seed(...items)
|
|
2629
|
+
|
|
2630
|
+
const page = await Union.paginate(
|
|
2631
|
+
{ first: 110 },
|
|
2632
|
+
{
|
|
2633
|
+
KeyConditionExpression: "PK = :pk",
|
|
2634
|
+
ExpressionAttributeValues: { ":pk": "PK" },
|
|
2635
|
+
}
|
|
2636
|
+
)
|
|
2637
|
+
expect(page.edges.length).toBe(100)
|
|
2638
|
+
|
|
2639
|
+
delete client.paginationOptions
|
|
2640
|
+
})
|
|
2473
2641
|
})
|
|
2474
2642
|
})
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import crypto from "crypto"
|
|
2
|
+
import { encodeDDBCursor, decodeDDBCursor } from "../pagination"
|
|
3
|
+
import { PaginationError } from "../errors"
|
|
4
|
+
|
|
5
|
+
describe("encodeDDBCursor", () => {
|
|
6
|
+
describe("basic encoding without encryption", () => {
|
|
7
|
+
it("should encode basic PK and SK", () => {
|
|
8
|
+
const result = encodeDDBCursor({
|
|
9
|
+
PK: "USER#123",
|
|
10
|
+
SK: "PROFILE#456",
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
expect(result).toMatchInlineSnapshot(
|
|
14
|
+
`"eyJQSyI6IlVTRVIjMTIzIiwiU0siOiJQUk9GSUxFIzQ1NiJ9"`
|
|
15
|
+
)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("should encode with GSI2 values", () => {
|
|
19
|
+
const result = encodeDDBCursor({
|
|
20
|
+
PK: "USER#123",
|
|
21
|
+
SK: "PROFILE#456",
|
|
22
|
+
GSI2PK: "GSI2PK#user123",
|
|
23
|
+
GSI2SK: "GSI2SK#profile456",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
expect(result).toMatchInlineSnapshot(
|
|
27
|
+
`"eyJQSyI6IlVTRVIjMTIzIiwiU0siOiJQUk9GSUxFIzQ1NiIsIkdTSTJQSyI6IkdTSTJQSyN1c2VyMTIzIiwiR1NJMlNLIjoiR1NJMlNLI3Byb2ZpbGU0NTYifQ=="`
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("should encode with multiple GSI values", () => {
|
|
32
|
+
const result = encodeDDBCursor({
|
|
33
|
+
PK: "USER#123",
|
|
34
|
+
SK: "PROFILE#456",
|
|
35
|
+
GSI2PK: "GSI2PK#user123",
|
|
36
|
+
GSI2SK: "GSI2SK#profile456",
|
|
37
|
+
GSI3PK: "GSI3PK#fixed",
|
|
38
|
+
GSI3SK: "GSI3SK#value",
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
expect(result).toMatchInlineSnapshot(
|
|
42
|
+
`"eyJQSyI6IlVTRVIjMTIzIiwiU0siOiJQUk9GSUxFIzQ1NiIsIkdTSTJQSyI6IkdTSTJQSyN1c2VyMTIzIiwiR1NJMlNLIjoiR1NJMlNLI3Byb2ZpbGU0NTYiLCJHU0kzUEsiOiJHU0kzUEsjZml4ZWQiLCJHU0kzU0siOiJHU0kzU0sjdmFsdWUifQ=="`
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe("encoding with encryption", () => {
|
|
48
|
+
const encryptionKey = crypto.randomBytes(32)
|
|
49
|
+
|
|
50
|
+
it("should encrypt the cursor when encryption key is provided", () => {
|
|
51
|
+
const result = encodeDDBCursor(
|
|
52
|
+
{
|
|
53
|
+
PK: "USER#123",
|
|
54
|
+
SK: "PROFILE#456",
|
|
55
|
+
},
|
|
56
|
+
encryptionKey
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
// The result should be encrypted and different from the unencrypted version
|
|
60
|
+
expect(result).not.toBe(
|
|
61
|
+
"eyJQSyI6IlVTRVIjMTIzIiwiU0siOiJQUk9GSUxFIzQ1NiJ9"
|
|
62
|
+
)
|
|
63
|
+
expect(result).toMatch(/^[A-Za-z0-9+/=]+$/) // Base64 format
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("should produce consistent encrypted results for same input", () => {
|
|
67
|
+
const input = {
|
|
68
|
+
PK: "USER#123",
|
|
69
|
+
SK: "PROFILE#456",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const result1 = encodeDDBCursor(input, encryptionKey)
|
|
73
|
+
const result2 = encodeDDBCursor(input, encryptionKey)
|
|
74
|
+
|
|
75
|
+
expect(result1).toBe(result2)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("should encrypt with GSI values", () => {
|
|
79
|
+
const result = encodeDDBCursor(
|
|
80
|
+
{
|
|
81
|
+
PK: "USER#123",
|
|
82
|
+
SK: "PROFILE#456",
|
|
83
|
+
GSI2PK: "GSI2PK#user123",
|
|
84
|
+
GSI2SK: "GSI2SK#profile456",
|
|
85
|
+
},
|
|
86
|
+
encryptionKey
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
expect(result).toMatch(/^[A-Za-z0-9+/=]+$/)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe("decodeDDBCursor", () => {
|
|
95
|
+
describe("basic decoding without encryption", () => {
|
|
96
|
+
it("should decode basic PK and SK", () => {
|
|
97
|
+
const encoded = "eyJQSyI6IlVTRVIjMTIzIiwiU0siOiJQUk9GSUxFIzQ1NiJ9"
|
|
98
|
+
const result = decodeDDBCursor(encoded)
|
|
99
|
+
|
|
100
|
+
expect(result).toMatchInlineSnapshot(`
|
|
101
|
+
Object {
|
|
102
|
+
"PK": "USER#123",
|
|
103
|
+
"SK": "PROFILE#456",
|
|
104
|
+
}
|
|
105
|
+
`)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("should decode with GSI2 values", () => {
|
|
109
|
+
const encoded =
|
|
110
|
+
"eyJQSyI6IlVTRVIjMTIzIiwiU0siOiJQUk9GSUxFIzQ1NiIsIkdTSTJQSyI6IkdTSTJQSyN1c2VyMTIzIiwiR1NJMlNLIjoiR1NJMlNLI3Byb2ZpbGU0NTYifQ=="
|
|
111
|
+
const result = decodeDDBCursor(encoded, "GSI2")
|
|
112
|
+
|
|
113
|
+
expect(result).toMatchInlineSnapshot(`
|
|
114
|
+
Object {
|
|
115
|
+
"GSI2PK": "GSI2PK#user123",
|
|
116
|
+
"GSI2SK": "GSI2SK#profile456",
|
|
117
|
+
"PK": "USER#123",
|
|
118
|
+
"SK": "PROFILE#456",
|
|
119
|
+
}
|
|
120
|
+
`)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("should decode with GSI3 values", () => {
|
|
124
|
+
const encoded =
|
|
125
|
+
"eyJQSyI6IlVTRVIjMTIzIiwiU0siOiJQUk9GSUxFIzQ1NiIsIkdTSTJQSyI6IkdTSTJQSyN1c2VyMTIzIiwiR1NJMlNLIjoiR1NJMlNLI3Byb2ZpbGU0NTYiLCJHU0kzUEsiOiJHU0kzUEsjZml4ZWQiLCJHU0kzU0siOiJHU0kzU0sjdmFsdWUifQ=="
|
|
126
|
+
const result = decodeDDBCursor(encoded, "GSI3")
|
|
127
|
+
|
|
128
|
+
expect(result).toMatchInlineSnapshot(`
|
|
129
|
+
Object {
|
|
130
|
+
"GSI3PK": "GSI3PK#fixed",
|
|
131
|
+
"GSI3SK": "GSI3SK#value",
|
|
132
|
+
"PK": "USER#123",
|
|
133
|
+
"SK": "PROFILE#456",
|
|
134
|
+
}
|
|
135
|
+
`)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe("decoding with encryption", () => {
|
|
140
|
+
const encryptionKey = crypto.randomBytes(32)
|
|
141
|
+
|
|
142
|
+
it("should decode encrypted cursor", () => {
|
|
143
|
+
const input = {
|
|
144
|
+
PK: "USER#123",
|
|
145
|
+
SK: "PROFILE#456",
|
|
146
|
+
}
|
|
147
|
+
const encoded = encodeDDBCursor(input, encryptionKey)
|
|
148
|
+
const result = decodeDDBCursor(encoded, undefined, encryptionKey)
|
|
149
|
+
|
|
150
|
+
expect(result).toMatchInlineSnapshot(`
|
|
151
|
+
Object {
|
|
152
|
+
"PK": "USER#123",
|
|
153
|
+
"SK": "PROFILE#456",
|
|
154
|
+
}
|
|
155
|
+
`)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it("should decode encrypted cursor with GSI values", () => {
|
|
159
|
+
const input = {
|
|
160
|
+
PK: "USER#123",
|
|
161
|
+
SK: "PROFILE#456",
|
|
162
|
+
GSI2PK: "GSI2PK#user123",
|
|
163
|
+
GSI2SK: "GSI2SK#profile456",
|
|
164
|
+
}
|
|
165
|
+
const encoded = encodeDDBCursor(input, encryptionKey)
|
|
166
|
+
const result = decodeDDBCursor(encoded, "GSI2", encryptionKey)
|
|
167
|
+
|
|
168
|
+
expect(result).toMatchInlineSnapshot(`
|
|
169
|
+
Object {
|
|
170
|
+
"GSI2PK": "GSI2PK#user123",
|
|
171
|
+
"GSI2SK": "GSI2SK#profile456",
|
|
172
|
+
"PK": "USER#123",
|
|
173
|
+
"SK": "PROFILE#456",
|
|
174
|
+
}
|
|
175
|
+
`)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it("should throw PaginationError when decryption fails", () => {
|
|
179
|
+
const wrongKey = crypto.randomBytes(32)
|
|
180
|
+
const input = {
|
|
181
|
+
PK: "USER#123",
|
|
182
|
+
SK: "PROFILE#456",
|
|
183
|
+
}
|
|
184
|
+
const encoded = encodeDDBCursor(input, encryptionKey)
|
|
185
|
+
|
|
186
|
+
expect(() => {
|
|
187
|
+
decodeDDBCursor(encoded, undefined, wrongKey)
|
|
188
|
+
}).toThrow(PaginationError)
|
|
189
|
+
expect(() => {
|
|
190
|
+
decodeDDBCursor(encoded, undefined, wrongKey)
|
|
191
|
+
}).toThrow("Couldn't decode cursor")
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe("error handling", () => {
|
|
196
|
+
it("should throw PaginationError for invalid base64", () => {
|
|
197
|
+
expect(() => {
|
|
198
|
+
decodeDDBCursor("invalid-base64")
|
|
199
|
+
}).toThrow(PaginationError)
|
|
200
|
+
expect(() => {
|
|
201
|
+
decodeDDBCursor("invalid-base64")
|
|
202
|
+
}).toThrow("Couldn't decode cursor")
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it("should throw PaginationError for invalid JSON", () => {
|
|
206
|
+
const invalidJson = Buffer.from("invalid json").toString("base64")
|
|
207
|
+
expect(() => {
|
|
208
|
+
decodeDDBCursor(invalidJson)
|
|
209
|
+
}).toThrow(PaginationError)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it("should throw PaginationError when PK is missing", () => {
|
|
213
|
+
const invalidData = Buffer.from(
|
|
214
|
+
JSON.stringify({ SK: "PROFILE#456" })
|
|
215
|
+
).toString("base64")
|
|
216
|
+
expect(() => {
|
|
217
|
+
decodeDDBCursor(invalidData)
|
|
218
|
+
}).toThrow(PaginationError)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("should throw PaginationError when SK is missing", () => {
|
|
222
|
+
const invalidData = Buffer.from(
|
|
223
|
+
JSON.stringify({ PK: "USER#123" })
|
|
224
|
+
).toString("base64")
|
|
225
|
+
expect(() => {
|
|
226
|
+
decodeDDBCursor(invalidData)
|
|
227
|
+
}).toThrow(PaginationError)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it("should throw PaginationError when PK is not a string", () => {
|
|
231
|
+
const invalidData = Buffer.from(
|
|
232
|
+
JSON.stringify({ PK: 123, SK: "PROFILE#456" })
|
|
233
|
+
).toString("base64")
|
|
234
|
+
expect(() => {
|
|
235
|
+
decodeDDBCursor(invalidData)
|
|
236
|
+
}).toThrow(PaginationError)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it("should throw PaginationError when SK is not a string", () => {
|
|
240
|
+
const invalidData = Buffer.from(
|
|
241
|
+
JSON.stringify({ PK: "USER#123", SK: 456 })
|
|
242
|
+
).toString("base64")
|
|
243
|
+
expect(() => {
|
|
244
|
+
decodeDDBCursor(invalidData)
|
|
245
|
+
}).toThrow(PaginationError)
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
describe("round-trip encoding and decoding", () => {
|
|
250
|
+
it("should round-trip basic values", () => {
|
|
251
|
+
const input = {
|
|
252
|
+
PK: "USER#123",
|
|
253
|
+
SK: "PROFILE#456",
|
|
254
|
+
}
|
|
255
|
+
const encoded = encodeDDBCursor(input)
|
|
256
|
+
const decoded = decodeDDBCursor(encoded)
|
|
257
|
+
|
|
258
|
+
expect(decoded).toEqual(input)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it("should round-trip with GSI values", () => {
|
|
262
|
+
const input = {
|
|
263
|
+
PK: "USER#123",
|
|
264
|
+
SK: "PROFILE#456",
|
|
265
|
+
GSI2PK: "GSI2PK#user123",
|
|
266
|
+
GSI2SK: "GSI2SK#profile456",
|
|
267
|
+
GSI3PK: "GSI3PK#fixed",
|
|
268
|
+
GSI3SK: "GSI3SK#value",
|
|
269
|
+
}
|
|
270
|
+
const encoded = encodeDDBCursor(input)
|
|
271
|
+
const decoded = decodeDDBCursor(encoded, "GSI2")
|
|
272
|
+
|
|
273
|
+
expect(decoded).toEqual({
|
|
274
|
+
PK: input.PK,
|
|
275
|
+
SK: input.SK,
|
|
276
|
+
GSI2PK: input.GSI2PK,
|
|
277
|
+
GSI2SK: input.GSI2SK,
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it("should round-trip with encryption", () => {
|
|
282
|
+
const encryptionKey = crypto.randomBytes(32)
|
|
283
|
+
const input = {
|
|
284
|
+
PK: "USER#123",
|
|
285
|
+
SK: "PROFILE#456",
|
|
286
|
+
GSI2PK: "GSI2PK#user123",
|
|
287
|
+
GSI2SK: "GSI2SK#profile456",
|
|
288
|
+
}
|
|
289
|
+
const encoded = encodeDDBCursor(input, encryptionKey)
|
|
290
|
+
const decoded = decodeDDBCursor(encoded, "GSI2", encryptionKey)
|
|
291
|
+
|
|
292
|
+
expect(decoded).toEqual({
|
|
293
|
+
PK: input.PK,
|
|
294
|
+
SK: input.SK,
|
|
295
|
+
GSI2PK: input.GSI2PK,
|
|
296
|
+
GSI2SK: input.GSI2SK,
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
})
|
package/src/client.ts
CHANGED
|
@@ -40,8 +40,10 @@ import {
|
|
|
40
40
|
encodeDDBCursor,
|
|
41
41
|
PaginationDirection,
|
|
42
42
|
PaginationInput,
|
|
43
|
+
PaginationOptions,
|
|
43
44
|
PaginationResult,
|
|
44
45
|
} from "./pagination"
|
|
46
|
+
import { GSI, GSI_NAMES, GSIPK, GSISK } from "./gsi"
|
|
45
47
|
|
|
46
48
|
export type QueryParams = Omit<
|
|
47
49
|
DocumentClient.QueryInput,
|
|
@@ -85,6 +87,11 @@ export interface ClientProps
|
|
|
85
87
|
* Must be a 32 character string, 256 bits.
|
|
86
88
|
*/
|
|
87
89
|
cursorEncryptionKey?: Buffer
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Defaults for pagination.
|
|
93
|
+
*/
|
|
94
|
+
paginationOptions?: PaginationOptions
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
export interface Key {
|
|
@@ -97,10 +104,12 @@ export class Client {
|
|
|
97
104
|
documentClient: DocumentClient
|
|
98
105
|
dataLoader: DataLoader<GetOperation<Decodable>, DynamoDBModelInstance, string>
|
|
99
106
|
cursorEncryptionKey?: Buffer
|
|
107
|
+
paginationOptions?: PaginationOptions
|
|
100
108
|
|
|
101
109
|
constructor(props: ClientProps) {
|
|
102
110
|
this.tableName = props?.tableName
|
|
103
111
|
this.cursorEncryptionKey = props?.cursorEncryptionKey
|
|
112
|
+
this.paginationOptions = props?.paginationOptions
|
|
104
113
|
this.documentClient = new DocumentClient(props)
|
|
105
114
|
this.dataLoader = new DataLoader<
|
|
106
115
|
GetOperation<Decodable>,
|
|
@@ -525,7 +534,10 @@ export class Client {
|
|
|
525
534
|
args: PaginationInput,
|
|
526
535
|
params: PaginationParams
|
|
527
536
|
): Promise<PaginationResult<DecodableInstance<M>>> {
|
|
528
|
-
const { cursor, limit, direction } = decodePagination(
|
|
537
|
+
const { cursor, limit, direction } = decodePagination(
|
|
538
|
+
args,
|
|
539
|
+
this.paginationOptions
|
|
540
|
+
)
|
|
529
541
|
|
|
530
542
|
const { results } = await this.query(
|
|
531
543
|
{
|
|
@@ -537,7 +549,7 @@ export class Client {
|
|
|
537
549
|
cursor,
|
|
538
550
|
// GSI1 is the inverse index and uses PK and SK (switched around)
|
|
539
551
|
params.IndexName && params.IndexName !== "GSI1"
|
|
540
|
-
? (params.IndexName as
|
|
552
|
+
? (params.IndexName as GSI)
|
|
541
553
|
: undefined,
|
|
542
554
|
this.cursorEncryptionKey
|
|
543
555
|
)
|
|
@@ -888,15 +900,8 @@ export class Client {
|
|
|
888
900
|
T extends {
|
|
889
901
|
PK: string
|
|
890
902
|
SK: string
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
GSI3PK?: string
|
|
894
|
-
GSI3SK?: string
|
|
895
|
-
GSI4PK?: string
|
|
896
|
-
GSI4SK?: string
|
|
897
|
-
GSI5PK?: string
|
|
898
|
-
GSI5SK?: string
|
|
899
|
-
}
|
|
903
|
+
} & { [key in GSIPK]?: string } &
|
|
904
|
+
{ [key in GSISK]?: string }
|
|
900
905
|
>(item: T): T {
|
|
901
906
|
const prefix = "$$DELETED$$"
|
|
902
907
|
|
|
@@ -908,14 +913,10 @@ export class Client {
|
|
|
908
913
|
...item,
|
|
909
914
|
PK: maybeWithPrefix(item.PK),
|
|
910
915
|
SK: maybeWithPrefix(item.SK),
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
GSI4PK: maybeWithPrefix(item.GSI4PK),
|
|
916
|
-
GSI4SK: maybeWithPrefix(item.GSI4SK),
|
|
917
|
-
GSI5PK: maybeWithPrefix(item.GSI5PK),
|
|
918
|
-
GSI5SK: maybeWithPrefix(item.GSI5SK),
|
|
916
|
+
...GSI_NAMES.map((GSI) => ({
|
|
917
|
+
[`${GSI}PK`]: maybeWithPrefix(item[`${GSI}PK` as const]),
|
|
918
|
+
[`${GSI}SK`]: maybeWithPrefix(item[`${GSI}SK` as const]),
|
|
919
|
+
})).reduce((acc, cur) => Object.assign(acc, cur), {}),
|
|
919
920
|
}
|
|
920
921
|
}
|
|
921
922
|
}
|
package/src/dynamodb-model.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ModelInstance, ModelConstructor, Union } from "@model-ts/core"
|
|
2
|
+
import { GSIPK, GSISK } from "./gsi"
|
|
2
3
|
|
|
3
4
|
export interface DynamoDBModelInstance extends ModelInstance<string, any> {
|
|
4
5
|
/**
|
|
@@ -7,15 +8,8 @@ export interface DynamoDBModelInstance extends ModelInstance<string, any> {
|
|
|
7
8
|
keys(): {
|
|
8
9
|
PK: string
|
|
9
10
|
SK: string
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
GSI3PK?: string
|
|
13
|
-
GSI3SK?: string
|
|
14
|
-
GSI4PK?: string
|
|
15
|
-
GSI4SK?: string
|
|
16
|
-
GSI5PK?: string
|
|
17
|
-
GSI5SK?: string
|
|
18
|
-
}
|
|
11
|
+
} & { [key in GSIPK]?: string } &
|
|
12
|
+
{ [key in GSISK]?: string }
|
|
19
13
|
|
|
20
14
|
PK: string
|
|
21
15
|
SK: string
|
|
@@ -27,6 +21,34 @@ export interface DynamoDBModelInstance extends ModelInstance<string, any> {
|
|
|
27
21
|
GSI4SK?: string
|
|
28
22
|
GSI5PK?: string
|
|
29
23
|
GSI5SK?: string
|
|
24
|
+
GSI6PK?: string
|
|
25
|
+
GSI6SK?: string
|
|
26
|
+
GSI7PK?: string
|
|
27
|
+
GSI7SK?: string
|
|
28
|
+
GSI8PK?: string
|
|
29
|
+
GSI8SK?: string
|
|
30
|
+
GSI9PK?: string
|
|
31
|
+
GSI9SK?: string
|
|
32
|
+
GSI10PK?: string
|
|
33
|
+
GSI10SK?: string
|
|
34
|
+
GSI11PK?: string
|
|
35
|
+
GSI11SK?: string
|
|
36
|
+
GSI12PK?: string
|
|
37
|
+
GSI12SK?: string
|
|
38
|
+
GSI13PK?: string
|
|
39
|
+
GSI13SK?: string
|
|
40
|
+
GSI14PK?: string
|
|
41
|
+
GSI14SK?: string
|
|
42
|
+
GSI15PK?: string
|
|
43
|
+
GSI15SK?: string
|
|
44
|
+
GSI16PK?: string
|
|
45
|
+
GSI16SK?: string
|
|
46
|
+
GSI17PK?: string
|
|
47
|
+
GSI17SK?: string
|
|
48
|
+
GSI18PK?: string
|
|
49
|
+
GSI18SK?: string
|
|
50
|
+
GSI19PK?: string
|
|
51
|
+
GSI19SK?: string
|
|
30
52
|
}
|
|
31
53
|
|
|
32
54
|
// export interface DynamoDBModel {}
|
package/src/gsi.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const GSI_NAMES = [
|
|
2
|
+
"GSI2",
|
|
3
|
+
"GSI3",
|
|
4
|
+
"GSI4",
|
|
5
|
+
"GSI5",
|
|
6
|
+
"GSI6",
|
|
7
|
+
"GSI7",
|
|
8
|
+
"GSI8",
|
|
9
|
+
"GSI9",
|
|
10
|
+
"GSI10",
|
|
11
|
+
"GSI11",
|
|
12
|
+
"GSI12",
|
|
13
|
+
"GSI13",
|
|
14
|
+
"GSI14",
|
|
15
|
+
"GSI15",
|
|
16
|
+
"GSI16",
|
|
17
|
+
"GSI17",
|
|
18
|
+
"GSI18",
|
|
19
|
+
"GSI19",
|
|
20
|
+
] as const
|
|
21
|
+
|
|
22
|
+
export type GSI = typeof GSI_NAMES[number]
|
|
23
|
+
|
|
24
|
+
export type GSIPK = `${GSI}PK`
|
|
25
|
+
export type GSISK = `${GSI}SK`
|
package/src/operations.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Decodable,
|
|
7
7
|
} from "./dynamodb-model"
|
|
8
8
|
import { Key } from "./client"
|
|
9
|
+
import { GSIPK, GSISK } from "./gsi"
|
|
9
10
|
|
|
10
11
|
export type Operation<
|
|
11
12
|
T extends DynamoDBModelInstance,
|
|
@@ -57,16 +58,9 @@ export interface UpdateRawOperation<M extends DynamoDBModelConstructor<any>>
|
|
|
57
58
|
_operation: "updateRaw"
|
|
58
59
|
_model: M
|
|
59
60
|
key: Key
|
|
60
|
-
attributes: Partial<TypeOf<M>> &
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
GSI3PK?: string | null
|
|
64
|
-
GSI3SK?: string | null
|
|
65
|
-
GSI4PK?: string | null
|
|
66
|
-
GSI4SK?: string | null
|
|
67
|
-
GSI5PK?: string | null
|
|
68
|
-
GSI5SK?: string | null
|
|
69
|
-
}
|
|
61
|
+
attributes: Partial<TypeOf<M>> &
|
|
62
|
+
{ [key in GSIPK]?: string | null } &
|
|
63
|
+
{ [key in GSISK]?: string | null }
|
|
70
64
|
}
|
|
71
65
|
|
|
72
66
|
// -------------------------------------------------------------------------------------
|