@model-ts/dynamodb 4.1.0 → 4.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.
- package/CHANGELOG.md +6 -0
- package/dist/cjs/__test__/client-env-guard.test.d.ts +1 -0
- package/dist/cjs/__test__/client-env-guard.test.js +28 -0
- package/dist/cjs/__test__/client-env-guard.test.js.map +1 -0
- package/dist/cjs/__test__/conformance.test.d.ts +1 -0
- package/dist/cjs/__test__/conformance.test.js +1835 -0
- package/dist/cjs/__test__/conformance.test.js.map +1 -0
- package/dist/cjs/__test__/in-memory.spec.test.d.ts +1 -0
- package/dist/cjs/__test__/in-memory.spec.test.js +185 -0
- package/dist/cjs/__test__/in-memory.spec.test.js.map +1 -0
- package/dist/cjs/client.d.ts +2 -2
- package/dist/cjs/client.js +14 -6
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/errors.d.ts +13 -1
- package/dist/cjs/errors.js +12 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/in-memory/document-client.d.ts +45 -0
- package/dist/cjs/in-memory/document-client.js +795 -0
- package/dist/cjs/in-memory/document-client.js.map +1 -0
- package/dist/cjs/in-memory/expression.d.ts +42 -0
- package/dist/cjs/in-memory/expression.js +585 -0
- package/dist/cjs/in-memory/expression.js.map +1 -0
- package/dist/cjs/in-memory/index.d.ts +2 -0
- package/dist/cjs/in-memory/index.js +6 -0
- package/dist/cjs/in-memory/index.js.map +1 -0
- package/dist/cjs/in-memory/spec.d.ts +23 -0
- package/dist/cjs/in-memory/spec.js +141 -0
- package/dist/cjs/in-memory/spec.js.map +1 -0
- package/dist/cjs/in-memory/store.d.ts +73 -0
- package/dist/cjs/in-memory/store.js +267 -0
- package/dist/cjs/in-memory/store.js.map +1 -0
- package/dist/cjs/in-memory/treap.d.ts +31 -0
- package/dist/cjs/in-memory/treap.js +187 -0
- package/dist/cjs/in-memory/treap.js.map +1 -0
- package/dist/cjs/in-memory/utils.d.ts +10 -0
- package/dist/cjs/in-memory/utils.js +38 -0
- package/dist/cjs/in-memory/utils.js.map +1 -0
- package/dist/cjs/sandbox.js +44 -0
- package/dist/cjs/sandbox.js.map +1 -1
- package/dist/esm/__test__/client-env-guard.test.d.ts +1 -0
- package/dist/esm/__test__/client-env-guard.test.js +26 -0
- package/dist/esm/__test__/client-env-guard.test.js.map +1 -0
- package/dist/esm/__test__/conformance.test.d.ts +1 -0
- package/dist/esm/__test__/conformance.test.js +1833 -0
- package/dist/esm/__test__/conformance.test.js.map +1 -0
- package/dist/esm/__test__/in-memory.spec.test.d.ts +1 -0
- package/dist/esm/__test__/in-memory.spec.test.js +183 -0
- package/dist/esm/__test__/in-memory.spec.test.js.map +1 -0
- package/dist/esm/client.d.ts +2 -2
- package/dist/esm/client.js +14 -6
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/errors.d.ts +13 -1
- package/dist/esm/errors.js +10 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/in-memory/document-client.d.ts +45 -0
- package/dist/esm/in-memory/document-client.js +791 -0
- package/dist/esm/in-memory/document-client.js.map +1 -0
- package/dist/esm/in-memory/expression.d.ts +42 -0
- package/dist/esm/in-memory/expression.js +577 -0
- package/dist/esm/in-memory/expression.js.map +1 -0
- package/dist/esm/in-memory/index.d.ts +2 -0
- package/dist/esm/in-memory/index.js +3 -0
- package/dist/esm/in-memory/index.js.map +1 -0
- package/dist/esm/in-memory/spec.d.ts +23 -0
- package/dist/esm/in-memory/spec.js +138 -0
- package/dist/esm/in-memory/spec.js.map +1 -0
- package/dist/esm/in-memory/store.d.ts +73 -0
- package/dist/esm/in-memory/store.js +258 -0
- package/dist/esm/in-memory/store.js.map +1 -0
- package/dist/esm/in-memory/treap.d.ts +31 -0
- package/dist/esm/in-memory/treap.js +183 -0
- package/dist/esm/in-memory/treap.js.map +1 -0
- package/dist/esm/in-memory/utils.d.ts +10 -0
- package/dist/esm/in-memory/utils.js +28 -0
- package/dist/esm/in-memory/utils.js.map +1 -0
- package/dist/esm/sandbox.js +44 -0
- package/dist/esm/sandbox.js.map +1 -1
- package/package.json +2 -1
- package/src/__test__/client-env-guard.test.ts +31 -0
- package/src/__test__/conformance.test.ts +2042 -0
- package/src/__test__/in-memory.spec.test.ts +230 -0
- package/src/client.ts +17 -4
- package/src/errors.ts +24 -0
- package/src/in-memory/document-client.ts +1140 -0
- package/src/in-memory/expression.ts +730 -0
- package/src/in-memory/index.ts +2 -0
- package/src/in-memory/spec.ts +159 -0
- package/src/in-memory/store.ts +360 -0
- package/src/in-memory/treap.ts +239 -0
- package/src/in-memory/utils.ts +45 -0
- package/src/sandbox.ts +56 -0
|
@@ -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
|
+
}
|