@model-ts/dynamodb 4.1.0 → 4.2.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/__test__/client-env-guard.test.d.ts +1 -0
  3. package/dist/cjs/__test__/client-env-guard.test.js +28 -0
  4. package/dist/cjs/__test__/client-env-guard.test.js.map +1 -0
  5. package/dist/cjs/__test__/conformance.test.d.ts +1 -0
  6. package/dist/cjs/__test__/conformance.test.js +1835 -0
  7. package/dist/cjs/__test__/conformance.test.js.map +1 -0
  8. package/dist/cjs/__test__/in-memory.spec.test.d.ts +1 -0
  9. package/dist/cjs/__test__/in-memory.spec.test.js +234 -0
  10. package/dist/cjs/__test__/in-memory.spec.test.js.map +1 -0
  11. package/dist/cjs/client.d.ts +2 -2
  12. package/dist/cjs/client.js +14 -6
  13. package/dist/cjs/client.js.map +1 -1
  14. package/dist/cjs/errors.d.ts +13 -1
  15. package/dist/cjs/errors.js +12 -1
  16. package/dist/cjs/errors.js.map +1 -1
  17. package/dist/cjs/in-memory/document-client.d.ts +45 -0
  18. package/dist/cjs/in-memory/document-client.js +795 -0
  19. package/dist/cjs/in-memory/document-client.js.map +1 -0
  20. package/dist/cjs/in-memory/expression.d.ts +42 -0
  21. package/dist/cjs/in-memory/expression.js +665 -0
  22. package/dist/cjs/in-memory/expression.js.map +1 -0
  23. package/dist/cjs/in-memory/index.d.ts +2 -0
  24. package/dist/cjs/in-memory/index.js +6 -0
  25. package/dist/cjs/in-memory/index.js.map +1 -0
  26. package/dist/cjs/in-memory/spec.d.ts +23 -0
  27. package/dist/cjs/in-memory/spec.js +141 -0
  28. package/dist/cjs/in-memory/spec.js.map +1 -0
  29. package/dist/cjs/in-memory/store.d.ts +73 -0
  30. package/dist/cjs/in-memory/store.js +267 -0
  31. package/dist/cjs/in-memory/store.js.map +1 -0
  32. package/dist/cjs/in-memory/treap.d.ts +31 -0
  33. package/dist/cjs/in-memory/treap.js +187 -0
  34. package/dist/cjs/in-memory/treap.js.map +1 -0
  35. package/dist/cjs/in-memory/utils.d.ts +10 -0
  36. package/dist/cjs/in-memory/utils.js +38 -0
  37. package/dist/cjs/in-memory/utils.js.map +1 -0
  38. package/dist/cjs/sandbox.js +44 -0
  39. package/dist/cjs/sandbox.js.map +1 -1
  40. package/dist/esm/__test__/client-env-guard.test.d.ts +1 -0
  41. package/dist/esm/__test__/client-env-guard.test.js +26 -0
  42. package/dist/esm/__test__/client-env-guard.test.js.map +1 -0
  43. package/dist/esm/__test__/conformance.test.d.ts +1 -0
  44. package/dist/esm/__test__/conformance.test.js +1833 -0
  45. package/dist/esm/__test__/conformance.test.js.map +1 -0
  46. package/dist/esm/__test__/in-memory.spec.test.d.ts +1 -0
  47. package/dist/esm/__test__/in-memory.spec.test.js +232 -0
  48. package/dist/esm/__test__/in-memory.spec.test.js.map +1 -0
  49. package/dist/esm/client.d.ts +2 -2
  50. package/dist/esm/client.js +14 -6
  51. package/dist/esm/client.js.map +1 -1
  52. package/dist/esm/errors.d.ts +13 -1
  53. package/dist/esm/errors.js +10 -0
  54. package/dist/esm/errors.js.map +1 -1
  55. package/dist/esm/in-memory/document-client.d.ts +45 -0
  56. package/dist/esm/in-memory/document-client.js +791 -0
  57. package/dist/esm/in-memory/document-client.js.map +1 -0
  58. package/dist/esm/in-memory/expression.d.ts +42 -0
  59. package/dist/esm/in-memory/expression.js +657 -0
  60. package/dist/esm/in-memory/expression.js.map +1 -0
  61. package/dist/esm/in-memory/index.d.ts +2 -0
  62. package/dist/esm/in-memory/index.js +3 -0
  63. package/dist/esm/in-memory/index.js.map +1 -0
  64. package/dist/esm/in-memory/spec.d.ts +23 -0
  65. package/dist/esm/in-memory/spec.js +138 -0
  66. package/dist/esm/in-memory/spec.js.map +1 -0
  67. package/dist/esm/in-memory/store.d.ts +73 -0
  68. package/dist/esm/in-memory/store.js +258 -0
  69. package/dist/esm/in-memory/store.js.map +1 -0
  70. package/dist/esm/in-memory/treap.d.ts +31 -0
  71. package/dist/esm/in-memory/treap.js +183 -0
  72. package/dist/esm/in-memory/treap.js.map +1 -0
  73. package/dist/esm/in-memory/utils.d.ts +10 -0
  74. package/dist/esm/in-memory/utils.js +28 -0
  75. package/dist/esm/in-memory/utils.js.map +1 -0
  76. package/dist/esm/sandbox.js +44 -0
  77. package/dist/esm/sandbox.js.map +1 -1
  78. package/package.json +2 -1
  79. package/src/__test__/client-env-guard.test.ts +31 -0
  80. package/src/__test__/conformance.test.ts +2042 -0
  81. package/src/__test__/in-memory.spec.test.ts +283 -0
  82. package/src/client.ts +17 -4
  83. package/src/errors.ts +24 -0
  84. package/src/in-memory/document-client.ts +1140 -0
  85. package/src/in-memory/expression.ts +830 -0
  86. package/src/in-memory/index.ts +2 -0
  87. package/src/in-memory/spec.ts +159 -0
  88. package/src/in-memory/store.ts +360 -0
  89. package/src/in-memory/treap.ts +239 -0
  90. package/src/in-memory/utils.ts +45 -0
  91. package/src/sandbox.ts +56 -0
@@ -0,0 +1,2 @@
1
+ export * from "./document-client"
2
+ export * from "./spec"
@@ -0,0 +1,159 @@
1
+ import { GSI_NAMES, GSI } from "../gsi"
2
+
3
+ export type InMemoryIndexName = "primary" | GSI
4
+
5
+ export interface InMemoryMethodSpec {
6
+ supportedParams: string[]
7
+ unsupportedParams?: string[]
8
+ }
9
+
10
+ export interface InMemorySpec {
11
+ version: string
12
+ scope: string
13
+ projection: "ALL"
14
+ excludedIndexes: string[]
15
+ indexes: InMemoryIndexName[]
16
+ methods: {
17
+ [method: string]: InMemoryMethodSpec
18
+ }
19
+ unsupportedMethods: string[]
20
+ }
21
+
22
+ export const IN_MEMORY_INDEXES: InMemoryIndexName[] = [
23
+ "primary",
24
+ ...GSI_NAMES,
25
+ ]
26
+
27
+ export const IN_MEMORY_SPEC: InMemorySpec = {
28
+ version: "2026-02-09",
29
+ scope: "model-ts/dynamodb",
30
+ projection: "ALL",
31
+ excludedIndexes: ["GSI1"],
32
+ indexes: IN_MEMORY_INDEXES,
33
+ methods: {
34
+ get: {
35
+ supportedParams: ["TableName", "Key", "ConsistentRead"],
36
+ unsupportedParams: [
37
+ "AttributesToGet",
38
+ "ProjectionExpression",
39
+ "ExpressionAttributeNames",
40
+ ],
41
+ },
42
+ put: {
43
+ supportedParams: [
44
+ "TableName",
45
+ "Item",
46
+ "ConditionExpression",
47
+ "ExpressionAttributeNames",
48
+ "ExpressionAttributeValues",
49
+ ],
50
+ unsupportedParams: [
51
+ "Expected",
52
+ "ReturnValues",
53
+ "ReturnConsumedCapacity",
54
+ "ReturnItemCollectionMetrics",
55
+ ],
56
+ },
57
+ update: {
58
+ supportedParams: [
59
+ "TableName",
60
+ "Key",
61
+ "ConditionExpression",
62
+ "UpdateExpression",
63
+ "ExpressionAttributeNames",
64
+ "ExpressionAttributeValues",
65
+ "ReturnValues",
66
+ ],
67
+ unsupportedParams: [
68
+ "Expected",
69
+ "AttributeUpdates",
70
+ "ReturnConsumedCapacity",
71
+ "ReturnItemCollectionMetrics",
72
+ ],
73
+ },
74
+ delete: {
75
+ supportedParams: [
76
+ "TableName",
77
+ "Key",
78
+ "ConditionExpression",
79
+ "ExpressionAttributeNames",
80
+ "ExpressionAttributeValues",
81
+ ],
82
+ unsupportedParams: [
83
+ "Expected",
84
+ "ReturnValues",
85
+ "ReturnConsumedCapacity",
86
+ "ReturnItemCollectionMetrics",
87
+ ],
88
+ },
89
+ query: {
90
+ supportedParams: [
91
+ "TableName",
92
+ "IndexName",
93
+ "KeyConditionExpression",
94
+ "FilterExpression",
95
+ "ExpressionAttributeNames",
96
+ "ExpressionAttributeValues",
97
+ "Limit",
98
+ "ExclusiveStartKey",
99
+ "ScanIndexForward",
100
+ "ConsistentRead",
101
+ ],
102
+ unsupportedParams: [
103
+ "Select",
104
+ "ProjectionExpression",
105
+ "KeyConditions",
106
+ "QueryFilter",
107
+ "ConditionalOperator",
108
+ "AttributesToGet",
109
+ ],
110
+ },
111
+ scan: {
112
+ supportedParams: [
113
+ "TableName",
114
+ "FilterExpression",
115
+ "ExpressionAttributeNames",
116
+ "ExpressionAttributeValues",
117
+ "Limit",
118
+ "ExclusiveStartKey",
119
+ ],
120
+ unsupportedParams: [
121
+ "ProjectionExpression",
122
+ "Segment",
123
+ "TotalSegments",
124
+ "Select",
125
+ "ScanFilter",
126
+ ],
127
+ },
128
+ batchGet: {
129
+ supportedParams: ["RequestItems"],
130
+ unsupportedParams: ["ReturnConsumedCapacity"],
131
+ },
132
+ batchWrite: {
133
+ supportedParams: ["RequestItems"],
134
+ unsupportedParams: ["ReturnConsumedCapacity", "ReturnItemCollectionMetrics"],
135
+ },
136
+ transactWrite: {
137
+ supportedParams: ["TransactItems"],
138
+ unsupportedParams: [
139
+ "ClientRequestToken",
140
+ "ReturnConsumedCapacity",
141
+ "ReturnItemCollectionMetrics",
142
+ ],
143
+ },
144
+ },
145
+ unsupportedMethods: [
146
+ "createSet",
147
+ "transactGet",
148
+ "putItem",
149
+ "deleteItem",
150
+ "updateItem",
151
+ "queryItems",
152
+ "scanItems",
153
+ ],
154
+ }
155
+
156
+ export const IN_MEMORY_CONDITIONS = {
157
+ excludedGSI: "GSI1 is intentionally excluded from in-memory mode.",
158
+ gsiProjection: "All GSIs are treated as full projection for in-scope behavior.",
159
+ }
@@ -0,0 +1,360 @@
1
+ import { GSI, GSI_NAMES } from "../gsi"
2
+ import {
3
+ ParsedKeyCondition,
4
+ RangeCondition,
5
+ compareValues,
6
+ matchesRangeCondition,
7
+ } from "./expression"
8
+ import { DeterministicTreap, TreapBounds } from "./treap"
9
+ import {
10
+ InMemoryItem,
11
+ cloneItem,
12
+ encodeIndexEntryKey,
13
+ encodeItemKey,
14
+ stablePriority,
15
+ sortItemsByPKSK,
16
+ } from "./utils"
17
+ import { InMemoryIndexName } from "./spec"
18
+
19
+ interface IndexDescriptor {
20
+ name: InMemoryIndexName
21
+ hashAttribute: string
22
+ rangeAttribute: string
23
+ }
24
+
25
+ export const PRIMARY_INDEX_NAME: InMemoryIndexName = "primary"
26
+
27
+ const INDEX_DESCRIPTORS: IndexDescriptor[] = [
28
+ {
29
+ name: PRIMARY_INDEX_NAME,
30
+ hashAttribute: "PK",
31
+ rangeAttribute: "SK",
32
+ },
33
+ ...GSI_NAMES.map((name) => ({
34
+ name,
35
+ hashAttribute: `${name}PK`,
36
+ rangeAttribute: `${name}SK`,
37
+ })),
38
+ ]
39
+
40
+ const INDEX_BY_NAME = Object.fromEntries(
41
+ INDEX_DESCRIPTORS.map((descriptor) => [descriptor.name, descriptor])
42
+ ) as Record<InMemoryIndexName, IndexDescriptor>
43
+
44
+ export interface QueryCandidate {
45
+ entryKey: string
46
+ itemKey: string
47
+ item: InMemoryItem
48
+ }
49
+
50
+ export interface QueryCursor {
51
+ itemKey: string
52
+ rangeKey: string
53
+ }
54
+
55
+ export class InMemoryTableState {
56
+ private readonly itemStore = new Map<string, InMemoryItem>()
57
+
58
+ private readonly indexes = new Map<
59
+ InMemoryIndexName,
60
+ Map<string, DeterministicTreap<string>>
61
+ >(
62
+ INDEX_DESCRIPTORS.map((descriptor) => [descriptor.name, new Map()])
63
+ )
64
+
65
+ cloneItemByKey(key: { PK: string; SK: string }): InMemoryItem | undefined {
66
+ return this.cloneItemByItemKey(encodeItemKey(key.PK, key.SK))
67
+ }
68
+
69
+ cloneItemByItemKey(itemKey: string): InMemoryItem | undefined {
70
+ const existing = this.itemStore.get(itemKey)
71
+ return existing ? cloneItem(existing) : undefined
72
+ }
73
+
74
+ put(item: InMemoryItem): InMemoryItem | undefined {
75
+ const key = this.getValidatedPrimaryKey(item)
76
+ const itemKey = encodeItemKey(key.PK, key.SK)
77
+ const previous = this.itemStore.get(itemKey)
78
+
79
+ if (previous) {
80
+ this.removeFromIndexes(itemKey, previous)
81
+ }
82
+
83
+ const stored = cloneItem(item)
84
+ this.itemStore.set(itemKey, stored)
85
+ this.addToIndexes(itemKey, stored)
86
+
87
+ return previous ? cloneItem(previous) : undefined
88
+ }
89
+
90
+ deleteByKey(key: { PK: string; SK: string }): InMemoryItem | undefined {
91
+ const itemKey = encodeItemKey(key.PK, key.SK)
92
+ const previous = this.itemStore.get(itemKey)
93
+
94
+ if (!previous) return undefined
95
+
96
+ this.itemStore.delete(itemKey)
97
+ this.removeFromIndexes(itemKey, previous)
98
+
99
+ return cloneItem(previous)
100
+ }
101
+
102
+ iterateQueryCandidates(args: {
103
+ indexName: InMemoryIndexName
104
+ hashKey: string
105
+ rangeCondition?: RangeCondition
106
+ scanIndexForward: boolean
107
+ exclusiveStartKey?: QueryCursor
108
+ }): IterableIterator<QueryCandidate> {
109
+ const descriptor = INDEX_BY_NAME[args.indexName]
110
+ const partition = this.indexes.get(args.indexName)?.get(args.hashKey)
111
+
112
+ if (!partition) {
113
+ return [][Symbol.iterator]()
114
+ }
115
+
116
+ const bounds = this.toTreapBounds(args.rangeCondition)
117
+ const direction = args.scanIndexForward ? "asc" : "desc"
118
+
119
+ const iterator = partition.iterate(direction, bounds)
120
+ const exclusiveStartEntryKey = args.exclusiveStartKey
121
+ ? encodeIndexEntryKey(args.exclusiveStartKey.rangeKey, args.exclusiveStartKey.itemKey)
122
+ : undefined
123
+
124
+ const table = this
125
+
126
+ function* generate(): IterableIterator<QueryCandidate> {
127
+ for (const { key: entryKey, value: itemKey } of iterator) {
128
+ if (exclusiveStartEntryKey) {
129
+ if (direction === "asc" && entryKey <= exclusiveStartEntryKey) {
130
+ continue
131
+ }
132
+
133
+ if (direction === "desc" && entryKey >= exclusiveStartEntryKey) {
134
+ continue
135
+ }
136
+ }
137
+
138
+ const item = table.itemStore.get(itemKey)
139
+ if (!item) continue
140
+
141
+ if (args.rangeCondition) {
142
+ const rangeValue = item[descriptor.rangeAttribute]
143
+ if (!matchesRangeCondition(rangeValue, args.rangeCondition)) continue
144
+ }
145
+
146
+ yield {
147
+ entryKey,
148
+ itemKey,
149
+ item: cloneItem(item),
150
+ }
151
+ }
152
+ }
153
+
154
+ return generate()
155
+ }
156
+
157
+ scanItems(exclusiveStartKey?: { PK: string; SK: string }): InMemoryItem[] {
158
+ const sorted = sortItemsByPKSK([...this.itemStore.values()].map(cloneItem))
159
+ if (!exclusiveStartKey) return sorted
160
+
161
+ const startPK = exclusiveStartKey.PK
162
+ const startSK = exclusiveStartKey.SK
163
+
164
+ return sorted.filter((item) => {
165
+ const pk = String(item.PK)
166
+ const sk = String(item.SK)
167
+
168
+ if (pk > startPK) return true
169
+ if (pk < startPK) return false
170
+
171
+ return sk > startSK
172
+ })
173
+ }
174
+
175
+ createQueryCursor(indexName: InMemoryIndexName, item: InMemoryItem): QueryCursor {
176
+ const descriptor = INDEX_BY_NAME[indexName]
177
+
178
+ return {
179
+ itemKey: encodeItemKey(String(item.PK), String(item.SK)),
180
+ rangeKey: String(item[descriptor.rangeAttribute]),
181
+ }
182
+ }
183
+
184
+ getIndexKeyFromItem(
185
+ indexName: InMemoryIndexName,
186
+ item: InMemoryItem
187
+ ): { hash: string; range: string } | null {
188
+ const descriptor = INDEX_BY_NAME[indexName]
189
+ const hash = item[descriptor.hashAttribute]
190
+ const range = item[descriptor.rangeAttribute]
191
+
192
+ if (indexName === PRIMARY_INDEX_NAME) {
193
+ if (typeof hash !== "string" || typeof range !== "string") return null
194
+ return { hash, range }
195
+ }
196
+
197
+ if (typeof hash !== "string" || typeof range !== "string") return null
198
+ return { hash, range }
199
+ }
200
+
201
+ getDescriptor(indexName: InMemoryIndexName): IndexDescriptor {
202
+ return INDEX_BY_NAME[indexName]
203
+ }
204
+
205
+ hasItem(key: { PK: string; SK: string }): boolean {
206
+ return this.itemStore.has(encodeItemKey(key.PK, key.SK))
207
+ }
208
+
209
+ snapshot(): { [key: string]: any } {
210
+ const entries = sortItemsByPKSK([...this.itemStore.values()]).map(cloneItem)
211
+
212
+ return Object.fromEntries(
213
+ entries.map((item) => [`${item.PK}__${item.SK}`, item])
214
+ )
215
+ }
216
+
217
+ clear() {
218
+ this.itemStore.clear()
219
+ for (const partitionMap of this.indexes.values()) {
220
+ partitionMap.clear()
221
+ }
222
+ }
223
+
224
+ private getValidatedPrimaryKey(item: InMemoryItem): { PK: string; SK: string } {
225
+ if (typeof item.PK !== "string" || typeof item.SK !== "string") {
226
+ throw new Error("Primary key attributes PK and SK must be strings.")
227
+ }
228
+
229
+ return { PK: item.PK, SK: item.SK }
230
+ }
231
+
232
+ private addToIndexes(itemKey: string, item: InMemoryItem) {
233
+ for (const descriptor of INDEX_DESCRIPTORS) {
234
+ const projected = this.getIndexKeyFromItem(descriptor.name, item)
235
+ if (!projected) continue
236
+
237
+ const partitionMap = this.indexes.get(descriptor.name)!
238
+ const tree =
239
+ partitionMap.get(projected.hash) ??
240
+ (() => {
241
+ const created = new DeterministicTreap<string>()
242
+ partitionMap.set(projected.hash, created)
243
+ return created
244
+ })()
245
+
246
+ const entryKey = encodeIndexEntryKey(projected.range, itemKey)
247
+ tree.insert(
248
+ entryKey,
249
+ itemKey,
250
+ stablePriority(descriptor.name, projected.hash, projected.range, itemKey)
251
+ )
252
+ }
253
+ }
254
+
255
+ private removeFromIndexes(itemKey: string, item: InMemoryItem) {
256
+ for (const descriptor of INDEX_DESCRIPTORS) {
257
+ const projected = this.getIndexKeyFromItem(descriptor.name, item)
258
+ if (!projected) continue
259
+
260
+ const partitionMap = this.indexes.get(descriptor.name)!
261
+ const tree = partitionMap.get(projected.hash)
262
+ if (!tree) continue
263
+
264
+ const entryKey = encodeIndexEntryKey(projected.range, itemKey)
265
+ tree.remove(entryKey)
266
+
267
+ if (tree.size === 0) {
268
+ partitionMap.delete(projected.hash)
269
+ }
270
+ }
271
+ }
272
+
273
+ private toTreapBounds(rangeCondition?: RangeCondition): TreapBounds {
274
+ if (!rangeCondition) return {}
275
+
276
+ switch (rangeCondition.type) {
277
+ case "begins_with": {
278
+ const lower = encodeIndexEntryKey(rangeCondition.value, "")
279
+ const upper = encodeIndexEntryKey(`${rangeCondition.value}\uffff`, "")
280
+ return {
281
+ lower: { key: lower, inclusive: true },
282
+ upper: { key: upper, inclusive: true },
283
+ }
284
+ }
285
+ case "between": {
286
+ const lower = encodeIndexEntryKey(String(rangeCondition.lower), "")
287
+ const upper = encodeIndexEntryKey(String(rangeCondition.upper), "\uffff")
288
+ return {
289
+ lower: { key: lower, inclusive: true },
290
+ upper: { key: upper, inclusive: true },
291
+ }
292
+ }
293
+ case "=": {
294
+ const key = String(rangeCondition.value)
295
+ return {
296
+ lower: { key: encodeIndexEntryKey(key, ""), inclusive: true },
297
+ upper: { key: encodeIndexEntryKey(key, "\uffff"), inclusive: true },
298
+ }
299
+ }
300
+ case ">":
301
+ return {
302
+ lower: {
303
+ key: encodeIndexEntryKey(String(rangeCondition.value), "\uffff"),
304
+ inclusive: false,
305
+ },
306
+ }
307
+ case ">=":
308
+ return {
309
+ lower: {
310
+ key: encodeIndexEntryKey(String(rangeCondition.value), ""),
311
+ inclusive: true,
312
+ },
313
+ }
314
+ case "<":
315
+ return {
316
+ upper: {
317
+ key: encodeIndexEntryKey(String(rangeCondition.value), ""),
318
+ inclusive: false,
319
+ },
320
+ }
321
+ case "<=":
322
+ return {
323
+ upper: {
324
+ key: encodeIndexEntryKey(String(rangeCondition.value), "\uffff"),
325
+ inclusive: true,
326
+ },
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ export const isGSI = (indexName: InMemoryIndexName): indexName is GSI =>
333
+ indexName !== PRIMARY_INDEX_NAME
334
+
335
+ export const parseIndexName = (indexName?: string): InMemoryIndexName =>
336
+ (indexName ?? PRIMARY_INDEX_NAME) as InMemoryIndexName
337
+
338
+ export const isSupportedIndexName = (indexName: string): indexName is InMemoryIndexName =>
339
+ indexName === PRIMARY_INDEX_NAME || GSI_NAMES.includes(indexName as GSI)
340
+
341
+ export const matchesKeyConditionDescriptor = (
342
+ indexName: InMemoryIndexName,
343
+ condition: ParsedKeyCondition
344
+ ): boolean => {
345
+ const descriptor = INDEX_BY_NAME[indexName]
346
+
347
+ if (condition.hashAttribute !== descriptor.hashAttribute) return false
348
+ if (!condition.range) return true
349
+
350
+ return condition.range.attribute === descriptor.rangeAttribute
351
+ }
352
+
353
+ export const compareItemKey = (
354
+ left: { PK: string; SK: string },
355
+ right: { PK: string; SK: string }
356
+ ): number => {
357
+ const pkCmp = compareValues(left.PK, right.PK)
358
+ if (pkCmp !== 0) return pkCmp
359
+ return compareValues(left.SK, right.SK)
360
+ }