@model-ts/dynamodb 4.0.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 +12 -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/__test__/rollback.test.d.ts +1 -0
- package/dist/cjs/__test__/rollback.test.js +196 -0
- package/dist/cjs/__test__/rollback.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.d.ts +2 -0
- package/dist/cjs/sandbox.js +172 -1
- 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/__test__/rollback.test.d.ts +1 -0
- package/dist/esm/__test__/rollback.test.js +194 -0
- package/dist/esm/__test__/rollback.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.d.ts +2 -0
- package/dist/esm/sandbox.js +172 -1
- 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/__test__/rollback.test.ts +279 -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 +227 -1
|
@@ -0,0 +1,1140 @@
|
|
|
1
|
+
import { NotSupportedError } from "../errors"
|
|
2
|
+
import {
|
|
3
|
+
evaluateConditionExpression,
|
|
4
|
+
parseKeyConditionExpression,
|
|
5
|
+
parseUpdateExpression,
|
|
6
|
+
} from "./expression"
|
|
7
|
+
import {
|
|
8
|
+
InMemoryTableState,
|
|
9
|
+
PRIMARY_INDEX_NAME,
|
|
10
|
+
isSupportedIndexName,
|
|
11
|
+
matchesKeyConditionDescriptor,
|
|
12
|
+
parseIndexName,
|
|
13
|
+
isGSI,
|
|
14
|
+
} from "./store"
|
|
15
|
+
import { IN_MEMORY_SPEC, InMemoryIndexName } from "./spec"
|
|
16
|
+
import { cloneItem, encodeItemKey } from "./utils"
|
|
17
|
+
|
|
18
|
+
interface PromiseRequest<T> {
|
|
19
|
+
promise: () => Promise<T>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type AnyParams = { [key: string]: any }
|
|
23
|
+
|
|
24
|
+
export interface InMemoryDocumentClient {
|
|
25
|
+
get(params: AnyParams): PromiseRequest<{ Item?: any }>
|
|
26
|
+
put(params: AnyParams): PromiseRequest<{}>
|
|
27
|
+
update(params: AnyParams): PromiseRequest<{ Attributes?: any }>
|
|
28
|
+
delete(params: AnyParams): PromiseRequest<{}>
|
|
29
|
+
query(params: AnyParams): PromiseRequest<{
|
|
30
|
+
Items?: any[]
|
|
31
|
+
Count: number
|
|
32
|
+
ScannedCount: number
|
|
33
|
+
LastEvaluatedKey?: any
|
|
34
|
+
}>
|
|
35
|
+
scan(params: AnyParams): PromiseRequest<{
|
|
36
|
+
Items?: any[]
|
|
37
|
+
Count: number
|
|
38
|
+
ScannedCount: number
|
|
39
|
+
LastEvaluatedKey?: any
|
|
40
|
+
}>
|
|
41
|
+
batchGet(params: AnyParams): PromiseRequest<{ Responses?: { [tableName: string]: any[] } }>
|
|
42
|
+
batchWrite(params: AnyParams): PromiseRequest<{ UnprocessedItems: { [tableName: string]: any[] } }>
|
|
43
|
+
transactWrite(params: AnyParams): PromiseRequest<{}>
|
|
44
|
+
__inMemorySnapshot(tableName: string): { [key: string]: any }
|
|
45
|
+
__inMemoryResetTable(tableName: string): void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class InMemoryDocumentClientImpl implements InMemoryDocumentClient {
|
|
49
|
+
private readonly tables = new Map<string, InMemoryTableState>()
|
|
50
|
+
|
|
51
|
+
get(params: AnyParams): PromiseRequest<{ Item?: any }> {
|
|
52
|
+
return this.request("get", params, async () => {
|
|
53
|
+
this.assertSupportedParams("get", params)
|
|
54
|
+
const tableName = this.getRequiredTableName(params)
|
|
55
|
+
const key = this.getRequiredKey(params, "Key")
|
|
56
|
+
|
|
57
|
+
const table = this.getTable(tableName)
|
|
58
|
+
const item = table.cloneItemByKey(key)
|
|
59
|
+
|
|
60
|
+
return item ? { Item: item } : {}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
put(params: AnyParams): PromiseRequest<{}> {
|
|
65
|
+
return this.request("put", params, async () => {
|
|
66
|
+
this.assertSupportedParams("put", params)
|
|
67
|
+
const tableName = this.getRequiredTableName(params)
|
|
68
|
+
const table = this.getTable(tableName)
|
|
69
|
+
|
|
70
|
+
const item = params.Item
|
|
71
|
+
if (!item || typeof item !== "object") {
|
|
72
|
+
throw new NotSupportedError({
|
|
73
|
+
method: "put",
|
|
74
|
+
featurePath: "put.Item",
|
|
75
|
+
reason: "Item must be an object.",
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.assertPrimaryKey(item, "put")
|
|
80
|
+
this.assertExpressionAttributeInputs(params, [params.ConditionExpression])
|
|
81
|
+
|
|
82
|
+
const existing = table.cloneItemByKey({ PK: item.PK, SK: item.SK })
|
|
83
|
+
this.assertCondition("put", params, existing)
|
|
84
|
+
|
|
85
|
+
table.put(item)
|
|
86
|
+
|
|
87
|
+
return {}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
update(params: AnyParams): PromiseRequest<{ Attributes?: any }> {
|
|
92
|
+
return this.request("update", params, async () => {
|
|
93
|
+
this.assertSupportedParams("update", params)
|
|
94
|
+
const tableName = this.getRequiredTableName(params)
|
|
95
|
+
const key = this.getRequiredKey(params, "Key")
|
|
96
|
+
|
|
97
|
+
if (typeof params.UpdateExpression !== "string") {
|
|
98
|
+
throw new NotSupportedError({
|
|
99
|
+
method: "update",
|
|
100
|
+
featurePath: "update.UpdateExpression",
|
|
101
|
+
reason: "UpdateExpression is required.",
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const returnValues = params.ReturnValues ?? "NONE"
|
|
106
|
+
if (!["NONE", "ALL_NEW"].includes(returnValues)) {
|
|
107
|
+
throw this.validationError(
|
|
108
|
+
`Unsupported ReturnValues value: ${returnValues}`
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
this.assertExpressionAttributeInputs(params, [
|
|
112
|
+
params.ConditionExpression,
|
|
113
|
+
params.UpdateExpression,
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
const table = this.getTable(tableName)
|
|
117
|
+
const existing = table.cloneItemByKey(key)
|
|
118
|
+
|
|
119
|
+
this.assertCondition("update", params, existing)
|
|
120
|
+
|
|
121
|
+
const base = existing ?? { PK: key.PK, SK: key.SK }
|
|
122
|
+
const parsed = this.parseUpdateExpressionOrValidationError(
|
|
123
|
+
params.UpdateExpression,
|
|
124
|
+
{
|
|
125
|
+
method: "update",
|
|
126
|
+
expressionAttributeNames: params.ExpressionAttributeNames,
|
|
127
|
+
expressionAttributeValues: params.ExpressionAttributeValues,
|
|
128
|
+
item: base,
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const next = cloneItem(base)
|
|
133
|
+
|
|
134
|
+
for (const assignment of parsed.set) {
|
|
135
|
+
if ((assignment.attribute === "PK" || assignment.attribute === "SK") && assignment.value !== next[assignment.attribute]) {
|
|
136
|
+
throw this.validationError(
|
|
137
|
+
`One or more parameter values were invalid: Cannot update attribute ${assignment.attribute}. This attribute is part of the key`
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
next[assignment.attribute] = assignment.value
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const attribute of parsed.remove) {
|
|
145
|
+
if (attribute === "PK" || attribute === "SK") {
|
|
146
|
+
throw this.validationError(
|
|
147
|
+
`One or more parameter values were invalid: Cannot update attribute ${attribute}. This attribute is part of the key`
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
delete next[attribute]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.assertPrimaryKey(next, "update")
|
|
155
|
+
table.put(next)
|
|
156
|
+
|
|
157
|
+
if (returnValues === "ALL_NEW") {
|
|
158
|
+
return { Attributes: cloneItem(next) }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {}
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
delete(params: AnyParams): PromiseRequest<{}> {
|
|
166
|
+
return this.request("delete", params, async () => {
|
|
167
|
+
this.assertSupportedParams("delete", params)
|
|
168
|
+
const tableName = this.getRequiredTableName(params)
|
|
169
|
+
const key = this.getRequiredKey(params, "Key")
|
|
170
|
+
|
|
171
|
+
const table = this.getTable(tableName)
|
|
172
|
+
const existing = table.cloneItemByKey(key)
|
|
173
|
+
this.assertExpressionAttributeInputs(params, [params.ConditionExpression])
|
|
174
|
+
|
|
175
|
+
this.assertCondition("delete", params, existing)
|
|
176
|
+
|
|
177
|
+
table.deleteByKey(key)
|
|
178
|
+
return {}
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
query(params: AnyParams): PromiseRequest<{
|
|
183
|
+
Items?: any[]
|
|
184
|
+
Count: number
|
|
185
|
+
ScannedCount: number
|
|
186
|
+
LastEvaluatedKey?: any
|
|
187
|
+
}> {
|
|
188
|
+
return this.request("query", params, async () => {
|
|
189
|
+
this.assertSupportedParams("query", params)
|
|
190
|
+
const tableName = this.getRequiredTableName(params)
|
|
191
|
+
const table = this.getTable(tableName)
|
|
192
|
+
|
|
193
|
+
if (typeof params.KeyConditionExpression !== "string") {
|
|
194
|
+
throw new NotSupportedError({
|
|
195
|
+
method: "query",
|
|
196
|
+
featurePath: "query.KeyConditionExpression",
|
|
197
|
+
reason: "KeyConditionExpression is required.",
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const indexName = this.resolveIndexName(params.IndexName)
|
|
202
|
+
|
|
203
|
+
if (params.ConsistentRead && isGSI(indexName)) {
|
|
204
|
+
throw this.validationError(
|
|
205
|
+
"Consistent read cannot be true when querying a GSI"
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
this.assertExpressionAttributeInputs(params, [
|
|
209
|
+
params.KeyConditionExpression,
|
|
210
|
+
params.FilterExpression,
|
|
211
|
+
])
|
|
212
|
+
|
|
213
|
+
const condition = this.parseKeyConditionOrValidationError(
|
|
214
|
+
params.KeyConditionExpression,
|
|
215
|
+
{
|
|
216
|
+
method: "query",
|
|
217
|
+
expressionAttributeNames: params.ExpressionAttributeNames,
|
|
218
|
+
expressionAttributeValues: params.ExpressionAttributeValues,
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if (!matchesKeyConditionDescriptor(indexName, condition)) {
|
|
223
|
+
throw new NotSupportedError({
|
|
224
|
+
method: "query",
|
|
225
|
+
featurePath: "query.KeyConditionExpression",
|
|
226
|
+
reason: "KeyConditionExpression attributes do not match the selected index.",
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (typeof condition.hashValue !== "string") {
|
|
231
|
+
throw new NotSupportedError({
|
|
232
|
+
method: "query",
|
|
233
|
+
featurePath: "query.KeyConditionExpression.partitionValue",
|
|
234
|
+
reason: "Partition key values must be strings.",
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const scanIndexForward = params.ScanIndexForward !== false
|
|
239
|
+
const limit = this.normalizeLimit(params.Limit, "query")
|
|
240
|
+
const exclusiveStartKey = params.ExclusiveStartKey
|
|
241
|
+
? this.resolveExclusiveStartKey(params.ExclusiveStartKey, indexName, table)
|
|
242
|
+
: undefined
|
|
243
|
+
|
|
244
|
+
const iterator = table.iterateQueryCandidates({
|
|
245
|
+
indexName,
|
|
246
|
+
hashKey: condition.hashValue,
|
|
247
|
+
rangeCondition: condition.range?.condition,
|
|
248
|
+
scanIndexForward,
|
|
249
|
+
exclusiveStartKey,
|
|
250
|
+
})[Symbol.iterator]()
|
|
251
|
+
|
|
252
|
+
const items: any[] = []
|
|
253
|
+
let scannedCount = 0
|
|
254
|
+
let lastEvaluatedKey: any | undefined
|
|
255
|
+
|
|
256
|
+
let next = iterator.next()
|
|
257
|
+
while (!next.done) {
|
|
258
|
+
const candidate = next.value
|
|
259
|
+
scannedCount += 1
|
|
260
|
+
|
|
261
|
+
const include = params.FilterExpression
|
|
262
|
+
? this.evaluateConditionOrValidationError(
|
|
263
|
+
params.FilterExpression,
|
|
264
|
+
candidate.item,
|
|
265
|
+
{
|
|
266
|
+
method: "query",
|
|
267
|
+
expressionAttributeNames: params.ExpressionAttributeNames,
|
|
268
|
+
expressionAttributeValues: params.ExpressionAttributeValues,
|
|
269
|
+
},
|
|
270
|
+
"FilterExpression"
|
|
271
|
+
)
|
|
272
|
+
: true
|
|
273
|
+
|
|
274
|
+
if (include) {
|
|
275
|
+
items.push(candidate.item)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (limit !== undefined && scannedCount >= limit) {
|
|
279
|
+
lastEvaluatedKey = this.buildLastEvaluatedKey(indexName, candidate.item)
|
|
280
|
+
break
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
next = iterator.next()
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
Items: items,
|
|
288
|
+
Count: items.length,
|
|
289
|
+
ScannedCount: scannedCount,
|
|
290
|
+
LastEvaluatedKey: lastEvaluatedKey,
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
scan(params: AnyParams): PromiseRequest<{
|
|
296
|
+
Items?: any[]
|
|
297
|
+
Count: number
|
|
298
|
+
ScannedCount: number
|
|
299
|
+
LastEvaluatedKey?: any
|
|
300
|
+
}> {
|
|
301
|
+
return this.request("scan", params, async () => {
|
|
302
|
+
this.assertSupportedParams("scan", params)
|
|
303
|
+
|
|
304
|
+
const tableName = this.getRequiredTableName(params)
|
|
305
|
+
const table = this.getTable(tableName)
|
|
306
|
+
const limit = this.normalizeLimit(params.Limit, "scan")
|
|
307
|
+
this.assertExpressionAttributeInputs(params, [params.FilterExpression])
|
|
308
|
+
|
|
309
|
+
const exclusiveStartKey = params.ExclusiveStartKey
|
|
310
|
+
? this.getRequiredKey({ Key: params.ExclusiveStartKey }, "ExclusiveStartKey")
|
|
311
|
+
: undefined
|
|
312
|
+
|
|
313
|
+
const all = table.scanItems(exclusiveStartKey)
|
|
314
|
+
|
|
315
|
+
const items: any[] = []
|
|
316
|
+
let scannedCount = 0
|
|
317
|
+
let lastEvaluatedKey: any | undefined
|
|
318
|
+
|
|
319
|
+
for (let index = 0; index < all.length; index += 1) {
|
|
320
|
+
const candidate = all[index]
|
|
321
|
+
scannedCount += 1
|
|
322
|
+
|
|
323
|
+
const include = params.FilterExpression
|
|
324
|
+
? this.evaluateConditionOrValidationError(
|
|
325
|
+
params.FilterExpression,
|
|
326
|
+
candidate,
|
|
327
|
+
{
|
|
328
|
+
method: "scan",
|
|
329
|
+
expressionAttributeNames: params.ExpressionAttributeNames,
|
|
330
|
+
expressionAttributeValues: params.ExpressionAttributeValues,
|
|
331
|
+
},
|
|
332
|
+
"FilterExpression"
|
|
333
|
+
)
|
|
334
|
+
: true
|
|
335
|
+
|
|
336
|
+
if (include) {
|
|
337
|
+
items.push(candidate)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (limit !== undefined && scannedCount >= limit) {
|
|
341
|
+
lastEvaluatedKey = { PK: candidate.PK, SK: candidate.SK }
|
|
342
|
+
break
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
Items: items,
|
|
348
|
+
Count: items.length,
|
|
349
|
+
ScannedCount: scannedCount,
|
|
350
|
+
LastEvaluatedKey: lastEvaluatedKey,
|
|
351
|
+
}
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
batchGet(params: AnyParams): PromiseRequest<{ Responses?: { [tableName: string]: any[] } }> {
|
|
356
|
+
return this.request("batchGet", params, async () => {
|
|
357
|
+
this.assertSupportedParams("batchGet", params)
|
|
358
|
+
|
|
359
|
+
if (!params.RequestItems || typeof params.RequestItems !== "object") {
|
|
360
|
+
throw new NotSupportedError({
|
|
361
|
+
method: "batchGet",
|
|
362
|
+
featurePath: "batchGet.RequestItems",
|
|
363
|
+
reason: "RequestItems is required.",
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const responses: { [tableName: string]: any[] } = {}
|
|
368
|
+
|
|
369
|
+
for (const [tableName, request] of Object.entries<any>(params.RequestItems)) {
|
|
370
|
+
if (!request || typeof request !== "object") {
|
|
371
|
+
throw new NotSupportedError({
|
|
372
|
+
method: "batchGet",
|
|
373
|
+
featurePath: "batchGet.RequestItems",
|
|
374
|
+
reason: "Each RequestItems entry must be an object.",
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const keys = Array.isArray(request.Keys) ? request.Keys : []
|
|
379
|
+
|
|
380
|
+
if (keys.length > 100) {
|
|
381
|
+
throw this.validationError(
|
|
382
|
+
"Too many items requested for the BatchGetItem call"
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const table = this.getTable(tableName)
|
|
387
|
+
const tableResponses: any[] = []
|
|
388
|
+
const unique = new Set<string>()
|
|
389
|
+
|
|
390
|
+
for (const key of keys) {
|
|
391
|
+
const parsedKey = this.getRequiredKey({ Key: key }, "Key")
|
|
392
|
+
const dedupKey = `${parsedKey.PK}::${parsedKey.SK}`
|
|
393
|
+
if (unique.has(dedupKey)) {
|
|
394
|
+
throw this.validationError(
|
|
395
|
+
"Provided list of item keys contains duplicates"
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
unique.add(dedupKey)
|
|
399
|
+
|
|
400
|
+
const item = table.cloneItemByKey(parsedKey)
|
|
401
|
+
if (item) tableResponses.push(item)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
responses[tableName] = tableResponses
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return { Responses: responses }
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
batchWrite(params: AnyParams): PromiseRequest<{ UnprocessedItems: { [tableName: string]: any[] } }> {
|
|
412
|
+
return this.request("batchWrite", params, async () => {
|
|
413
|
+
this.assertSupportedParams("batchWrite", params)
|
|
414
|
+
|
|
415
|
+
if (!params.RequestItems || typeof params.RequestItems !== "object") {
|
|
416
|
+
throw new NotSupportedError({
|
|
417
|
+
method: "batchWrite",
|
|
418
|
+
featurePath: "batchWrite.RequestItems",
|
|
419
|
+
reason: "RequestItems is required.",
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
for (const [tableName, requests] of Object.entries<any[]>(params.RequestItems)) {
|
|
424
|
+
if (!Array.isArray(requests)) {
|
|
425
|
+
throw new NotSupportedError({
|
|
426
|
+
method: "batchWrite",
|
|
427
|
+
featurePath: "batchWrite.RequestItems",
|
|
428
|
+
reason: "Each RequestItems entry must be an array.",
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (requests.length > 25) {
|
|
433
|
+
throw this.validationError(
|
|
434
|
+
"Too many items requested for the BatchWriteItem call"
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const table = this.getTable(tableName)
|
|
439
|
+
|
|
440
|
+
for (const entry of requests) {
|
|
441
|
+
if (entry.PutRequest) {
|
|
442
|
+
const item = entry.PutRequest.Item
|
|
443
|
+
if (!item || typeof item !== "object") {
|
|
444
|
+
throw new NotSupportedError({
|
|
445
|
+
method: "batchWrite",
|
|
446
|
+
featurePath: "batchWrite.RequestItems.PutRequest.Item",
|
|
447
|
+
reason: "PutRequest.Item must be an object.",
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
this.assertPrimaryKey(item, "batchWrite")
|
|
452
|
+
table.put(item)
|
|
453
|
+
continue
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (entry.DeleteRequest) {
|
|
457
|
+
const key = this.getRequiredKey(
|
|
458
|
+
{ Key: entry.DeleteRequest.Key },
|
|
459
|
+
"DeleteRequest.Key"
|
|
460
|
+
)
|
|
461
|
+
table.deleteByKey(key)
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
throw this.validationError(
|
|
466
|
+
"Supplied AttributeValue has more than one datatypes set, must contain exactly one of the supported datatypes"
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return { UnprocessedItems: {} }
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
transactWrite(params: AnyParams): PromiseRequest<{}> {
|
|
476
|
+
return this.request("transactWrite", params, async () => {
|
|
477
|
+
this.assertSupportedParams("transactWrite", params)
|
|
478
|
+
|
|
479
|
+
const transactItems = params.TransactItems
|
|
480
|
+
if (!Array.isArray(transactItems) || transactItems.length === 0) {
|
|
481
|
+
throw new NotSupportedError({
|
|
482
|
+
method: "transactWrite",
|
|
483
|
+
featurePath: "transactWrite.TransactItems",
|
|
484
|
+
reason: "TransactItems must be a non-empty array.",
|
|
485
|
+
})
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (transactItems.length > 100) {
|
|
489
|
+
throw this.validationError(
|
|
490
|
+
"Member must have length less than or equal to 100"
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const touched = new Set<string>()
|
|
495
|
+
const journal = new Map<string, { tableName: string; key: { PK: string; SK: string }; before: any | null }>()
|
|
496
|
+
|
|
497
|
+
const remember = (tableName: string, key: { PK: string; SK: string }, before: any | undefined) => {
|
|
498
|
+
const marker = `${tableName}::${key.PK}::${key.SK}`
|
|
499
|
+
if (!journal.has(marker)) {
|
|
500
|
+
journal.set(marker, {
|
|
501
|
+
tableName,
|
|
502
|
+
key,
|
|
503
|
+
before: before ? cloneItem(before) : null,
|
|
504
|
+
})
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const ensureNoDuplicate = (tableName: string, key: { PK: string; SK: string }) => {
|
|
509
|
+
const marker = `${tableName}::${key.PK}::${key.SK}`
|
|
510
|
+
if (touched.has(marker)) {
|
|
511
|
+
throw this.validationError(
|
|
512
|
+
"Transaction request cannot include multiple operations on one item"
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
touched.add(marker)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const rollback = () => {
|
|
520
|
+
for (const entry of [...journal.values()].reverse()) {
|
|
521
|
+
const table = this.getTable(entry.tableName)
|
|
522
|
+
|
|
523
|
+
if (entry.before === null) {
|
|
524
|
+
table.deleteByKey(entry.key)
|
|
525
|
+
} else {
|
|
526
|
+
table.put(entry.before)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
for (const transactionEntry of transactItems) {
|
|
533
|
+
if (transactionEntry.Put) {
|
|
534
|
+
const put = transactionEntry.Put
|
|
535
|
+
const tableName = this.getRequiredTableName(put)
|
|
536
|
+
const table = this.getTable(tableName)
|
|
537
|
+
const item = put.Item
|
|
538
|
+
|
|
539
|
+
if (!item || typeof item !== "object") {
|
|
540
|
+
throw new NotSupportedError({
|
|
541
|
+
method: "transactWrite",
|
|
542
|
+
featurePath: "transactWrite.TransactItems.Put.Item",
|
|
543
|
+
reason: "Put.Item must be an object.",
|
|
544
|
+
})
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
this.assertPrimaryKey(item, "transactWrite")
|
|
548
|
+
const key = { PK: item.PK, SK: item.SK }
|
|
549
|
+
ensureNoDuplicate(tableName, key)
|
|
550
|
+
|
|
551
|
+
const existing = table.cloneItemByKey(key)
|
|
552
|
+
this.assertExpressionAttributeInputs(put, [put.ConditionExpression])
|
|
553
|
+
this.assertCondition("transactWrite", put, existing)
|
|
554
|
+
|
|
555
|
+
remember(tableName, key, existing)
|
|
556
|
+
table.put(item)
|
|
557
|
+
|
|
558
|
+
continue
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (transactionEntry.Update) {
|
|
562
|
+
const update = transactionEntry.Update
|
|
563
|
+
const tableName = this.getRequiredTableName(update)
|
|
564
|
+
const key = this.getRequiredKey(update, "Key")
|
|
565
|
+
ensureNoDuplicate(tableName, key)
|
|
566
|
+
|
|
567
|
+
const table = this.getTable(tableName)
|
|
568
|
+
const existing = table.cloneItemByKey(key)
|
|
569
|
+
this.assertExpressionAttributeInputs(update, [
|
|
570
|
+
update.ConditionExpression,
|
|
571
|
+
update.UpdateExpression,
|
|
572
|
+
])
|
|
573
|
+
this.assertCondition("transactWrite", update, existing)
|
|
574
|
+
|
|
575
|
+
if (typeof update.UpdateExpression !== "string") {
|
|
576
|
+
throw new NotSupportedError({
|
|
577
|
+
method: "transactWrite",
|
|
578
|
+
featurePath: "transactWrite.TransactItems.Update.UpdateExpression",
|
|
579
|
+
reason: "UpdateExpression is required.",
|
|
580
|
+
})
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const base = existing ?? { PK: key.PK, SK: key.SK }
|
|
584
|
+
const parsed = this.parseUpdateExpressionOrValidationError(
|
|
585
|
+
update.UpdateExpression,
|
|
586
|
+
{
|
|
587
|
+
method: "transactWrite",
|
|
588
|
+
expressionAttributeNames: update.ExpressionAttributeNames,
|
|
589
|
+
expressionAttributeValues: update.ExpressionAttributeValues,
|
|
590
|
+
item: base,
|
|
591
|
+
}
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
const next = cloneItem(base)
|
|
595
|
+
|
|
596
|
+
for (const assignment of parsed.set) {
|
|
597
|
+
if ((assignment.attribute === "PK" || assignment.attribute === "SK") && assignment.value !== next[assignment.attribute]) {
|
|
598
|
+
throw this.validationError(
|
|
599
|
+
`One or more parameter values were invalid: Cannot update attribute ${assignment.attribute}. This attribute is part of the key`
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
next[assignment.attribute] = assignment.value
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
for (const attribute of parsed.remove) {
|
|
607
|
+
if (attribute === "PK" || attribute === "SK") {
|
|
608
|
+
throw this.validationError(
|
|
609
|
+
`One or more parameter values were invalid: Cannot update attribute ${attribute}. This attribute is part of the key`
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
delete next[attribute]
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
this.assertPrimaryKey(next, "transactWrite")
|
|
617
|
+
|
|
618
|
+
remember(tableName, key, existing)
|
|
619
|
+
table.put(next)
|
|
620
|
+
|
|
621
|
+
continue
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (transactionEntry.Delete) {
|
|
625
|
+
const del = transactionEntry.Delete
|
|
626
|
+
const tableName = this.getRequiredTableName(del)
|
|
627
|
+
const key = this.getRequiredKey(del, "Key")
|
|
628
|
+
ensureNoDuplicate(tableName, key)
|
|
629
|
+
|
|
630
|
+
const table = this.getTable(tableName)
|
|
631
|
+
const existing = table.cloneItemByKey(key)
|
|
632
|
+
this.assertExpressionAttributeInputs(del, [del.ConditionExpression])
|
|
633
|
+
this.assertCondition("transactWrite", del, existing)
|
|
634
|
+
remember(tableName, key, existing)
|
|
635
|
+
|
|
636
|
+
table.deleteByKey(key)
|
|
637
|
+
continue
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (transactionEntry.ConditionCheck) {
|
|
641
|
+
const check = transactionEntry.ConditionCheck
|
|
642
|
+
const tableName = this.getRequiredTableName(check)
|
|
643
|
+
const key = this.getRequiredKey(check, "Key")
|
|
644
|
+
ensureNoDuplicate(tableName, key)
|
|
645
|
+
|
|
646
|
+
const table = this.getTable(tableName)
|
|
647
|
+
const existing = table.cloneItemByKey(key)
|
|
648
|
+
this.assertExpressionAttributeInputs(check, [check.ConditionExpression])
|
|
649
|
+
this.assertCondition("transactWrite", check, existing)
|
|
650
|
+
continue
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
throw new NotSupportedError({
|
|
654
|
+
method: "transactWrite",
|
|
655
|
+
featurePath: "transactWrite.TransactItems",
|
|
656
|
+
reason: "Each transaction entry must include Put, Update, Delete, or ConditionCheck.",
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
} catch (error: any) {
|
|
660
|
+
rollback()
|
|
661
|
+
|
|
662
|
+
if (error instanceof NotSupportedError) throw error
|
|
663
|
+
|
|
664
|
+
if (
|
|
665
|
+
error?.code === "ValidationException" &&
|
|
666
|
+
/Cannot update attribute (PK|SK)\. This attribute is part of the key/.test(
|
|
667
|
+
String(error?.message ?? "")
|
|
668
|
+
)
|
|
669
|
+
) {
|
|
670
|
+
throw this.transactionCanceledError(
|
|
671
|
+
"Transaction cancelled, please refer cancellation reasons for specific reasons [ValidationError]"
|
|
672
|
+
)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (error?.code === "ValidationException") {
|
|
676
|
+
throw error
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (error?.code === "ConditionalCheckFailedException") {
|
|
680
|
+
throw this.transactionCanceledError(
|
|
681
|
+
"Transaction cancelled, please refer cancellation reasons for specific reasons [None, ConditionalCheckFailed]"
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (error?.code === "TransactionCanceledException") {
|
|
686
|
+
throw error
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
throw this.transactionCanceledError(error?.message ?? "Transaction failed.")
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return {}
|
|
693
|
+
})
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
__inMemorySnapshot(tableName: string): { [key: string]: any } {
|
|
697
|
+
return this.getTable(tableName).snapshot()
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
__inMemoryResetTable(tableName: string): void {
|
|
701
|
+
this.getTable(tableName).clear()
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private request<T>(method: string, params: AnyParams, fn: () => Promise<T>): PromiseRequest<T> {
|
|
705
|
+
return {
|
|
706
|
+
promise: async () => {
|
|
707
|
+
try {
|
|
708
|
+
return await fn()
|
|
709
|
+
} catch (error) {
|
|
710
|
+
throw error
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
private getTable(tableName: string): InMemoryTableState {
|
|
717
|
+
const existing = this.tables.get(tableName)
|
|
718
|
+
if (existing) return existing
|
|
719
|
+
|
|
720
|
+
const next = new InMemoryTableState()
|
|
721
|
+
this.tables.set(tableName, next)
|
|
722
|
+
return next
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
private assertSupportedParams(method: string, params: AnyParams) {
|
|
726
|
+
const spec = IN_MEMORY_SPEC.methods[method]
|
|
727
|
+
|
|
728
|
+
if (!spec) {
|
|
729
|
+
throw new NotSupportedError({
|
|
730
|
+
method,
|
|
731
|
+
featurePath: method,
|
|
732
|
+
reason: `Unsupported method: ${method}`,
|
|
733
|
+
})
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
for (const [key, value] of Object.entries(params)) {
|
|
737
|
+
if (typeof value === "undefined") continue
|
|
738
|
+
|
|
739
|
+
if (spec.unsupportedParams?.includes(key)) {
|
|
740
|
+
throw new NotSupportedError({
|
|
741
|
+
method,
|
|
742
|
+
featurePath: `${method}.${key}`,
|
|
743
|
+
reason: `${key} is not supported in in-memory mode.`,
|
|
744
|
+
})
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (!spec.supportedParams.includes(key)) {
|
|
748
|
+
throw new NotSupportedError({
|
|
749
|
+
method,
|
|
750
|
+
featurePath: `${method}.${key}`,
|
|
751
|
+
reason: `Unsupported parameter: ${key}`,
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
private getRequiredTableName(params: AnyParams): string {
|
|
758
|
+
if (typeof params.TableName !== "string" || !params.TableName) {
|
|
759
|
+
throw new NotSupportedError({
|
|
760
|
+
method: "documentClient",
|
|
761
|
+
featurePath: "TableName",
|
|
762
|
+
reason: "TableName must be a non-empty string.",
|
|
763
|
+
})
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return params.TableName
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private getRequiredKey(
|
|
770
|
+
params: AnyParams,
|
|
771
|
+
featurePath: string
|
|
772
|
+
): { PK: string; SK: string } {
|
|
773
|
+
const key = params.Key
|
|
774
|
+
|
|
775
|
+
if (!key || typeof key !== "object") {
|
|
776
|
+
throw new NotSupportedError({
|
|
777
|
+
method: "documentClient",
|
|
778
|
+
featurePath,
|
|
779
|
+
reason: "Key must be an object.",
|
|
780
|
+
})
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (typeof key.PK !== "string" || typeof key.SK !== "string") {
|
|
784
|
+
throw this.validationError(
|
|
785
|
+
"One or more parameter values were invalid: Type mismatch for key"
|
|
786
|
+
)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return { PK: key.PK, SK: key.SK }
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
private assertPrimaryKey(item: any, method: string) {
|
|
793
|
+
if (typeof item.PK !== "string" || typeof item.SK !== "string") {
|
|
794
|
+
throw this.validationError(
|
|
795
|
+
"One or more parameter values were invalid: Type mismatch for key"
|
|
796
|
+
)
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
private assertCondition(method: string, params: AnyParams, item?: any) {
|
|
801
|
+
if (!params.ConditionExpression) return
|
|
802
|
+
|
|
803
|
+
const ok = this.evaluateConditionOrValidationError(
|
|
804
|
+
params.ConditionExpression,
|
|
805
|
+
item,
|
|
806
|
+
{
|
|
807
|
+
method,
|
|
808
|
+
expressionAttributeNames: params.ExpressionAttributeNames,
|
|
809
|
+
expressionAttributeValues: params.ExpressionAttributeValues,
|
|
810
|
+
},
|
|
811
|
+
"ConditionExpression"
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
if (!ok) {
|
|
815
|
+
throw this.awsError(
|
|
816
|
+
"ConditionalCheckFailedException",
|
|
817
|
+
"The conditional request failed."
|
|
818
|
+
)
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
private resolveIndexName(indexName?: string): InMemoryIndexName {
|
|
823
|
+
if (!indexName) return PRIMARY_INDEX_NAME
|
|
824
|
+
if (indexName === "GSI1") {
|
|
825
|
+
throw new NotSupportedError({
|
|
826
|
+
method: "query",
|
|
827
|
+
featurePath: "query.IndexName",
|
|
828
|
+
reason: "GSI1 is intentionally excluded from in-memory mode.",
|
|
829
|
+
})
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (!isSupportedIndexName(indexName)) {
|
|
833
|
+
throw new NotSupportedError({
|
|
834
|
+
method: "query",
|
|
835
|
+
featurePath: "query.IndexName",
|
|
836
|
+
reason: `Unsupported index: ${indexName}`,
|
|
837
|
+
})
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return parseIndexName(indexName)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
private resolveExclusiveStartKey(
|
|
844
|
+
startKey: any,
|
|
845
|
+
indexName: InMemoryIndexName,
|
|
846
|
+
table: InMemoryTableState
|
|
847
|
+
) {
|
|
848
|
+
const key = this.getRequiredKey({ Key: startKey }, "query.ExclusiveStartKey")
|
|
849
|
+
|
|
850
|
+
const descriptor = table.getDescriptor(indexName)
|
|
851
|
+
const rangeKey = startKey?.[descriptor.rangeAttribute]
|
|
852
|
+
const hashKey = startKey?.[descriptor.hashAttribute]
|
|
853
|
+
|
|
854
|
+
if (typeof rangeKey !== "string" || typeof hashKey !== "string") {
|
|
855
|
+
throw this.validationError(
|
|
856
|
+
"Exclusive Start Key must have same size as table's key schema"
|
|
857
|
+
)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return {
|
|
861
|
+
itemKey: encodeItemKey(key.PK, key.SK),
|
|
862
|
+
rangeKey,
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
private buildLastEvaluatedKey(indexName: InMemoryIndexName, item: any): any {
|
|
867
|
+
const key: any = {
|
|
868
|
+
PK: item.PK,
|
|
869
|
+
SK: item.SK,
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (indexName !== PRIMARY_INDEX_NAME) {
|
|
873
|
+
key[`${indexName}PK`] = item[`${indexName}PK`]
|
|
874
|
+
key[`${indexName}SK`] = item[`${indexName}SK`]
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return key
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
private normalizeLimit(value: any, method: "query" | "scan"): number | undefined {
|
|
881
|
+
if (typeof value === "undefined") return undefined
|
|
882
|
+
|
|
883
|
+
if (
|
|
884
|
+
typeof value !== "number" ||
|
|
885
|
+
!Number.isFinite(value) ||
|
|
886
|
+
value < 1
|
|
887
|
+
) {
|
|
888
|
+
throw this.validationError(
|
|
889
|
+
"Limit must be greater than or equal to 1"
|
|
890
|
+
)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return Math.floor(value)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
private awsError(code: string, message: string): Error & { code: string } {
|
|
897
|
+
const error = new Error(message) as Error & { code: string }
|
|
898
|
+
error.code = code
|
|
899
|
+
error.name = code
|
|
900
|
+
return error
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
private transactionCanceledError(message: string): Error & { code: string } {
|
|
904
|
+
return this.awsError("TransactionCanceledException", message)
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private validationError(message: string): Error & { code: string } {
|
|
908
|
+
return this.awsError("ValidationException", message)
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
private parseKeyConditionOrValidationError(
|
|
912
|
+
expression: string,
|
|
913
|
+
context: {
|
|
914
|
+
method: string
|
|
915
|
+
expressionAttributeNames?: { [key: string]: string }
|
|
916
|
+
expressionAttributeValues?: { [key: string]: any }
|
|
917
|
+
}
|
|
918
|
+
) {
|
|
919
|
+
try {
|
|
920
|
+
return parseKeyConditionExpression(expression, context)
|
|
921
|
+
} catch (error) {
|
|
922
|
+
if (error instanceof NotSupportedError) {
|
|
923
|
+
throw this.validationError(
|
|
924
|
+
this.toDynamoExpressionValidationMessage(
|
|
925
|
+
"KeyConditionExpression",
|
|
926
|
+
error
|
|
927
|
+
)
|
|
928
|
+
)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
throw error
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
private parseUpdateExpressionOrValidationError(
|
|
936
|
+
expression: string,
|
|
937
|
+
context: {
|
|
938
|
+
method: string
|
|
939
|
+
expressionAttributeNames?: { [key: string]: string }
|
|
940
|
+
expressionAttributeValues?: { [key: string]: any }
|
|
941
|
+
item?: any
|
|
942
|
+
}
|
|
943
|
+
) {
|
|
944
|
+
try {
|
|
945
|
+
return parseUpdateExpression(expression, context)
|
|
946
|
+
} catch (error) {
|
|
947
|
+
if (error instanceof NotSupportedError) {
|
|
948
|
+
throw this.validationError(
|
|
949
|
+
this.toDynamoExpressionValidationMessage("UpdateExpression", error)
|
|
950
|
+
)
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
throw error
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
private evaluateConditionOrValidationError(
|
|
958
|
+
expression: string,
|
|
959
|
+
item: any,
|
|
960
|
+
context: {
|
|
961
|
+
method: string
|
|
962
|
+
expressionAttributeNames?: { [key: string]: string }
|
|
963
|
+
expressionAttributeValues?: { [key: string]: any }
|
|
964
|
+
},
|
|
965
|
+
expressionType: "ConditionExpression" | "FilterExpression"
|
|
966
|
+
): boolean {
|
|
967
|
+
try {
|
|
968
|
+
return evaluateConditionExpression(expression, item, context)
|
|
969
|
+
} catch (error) {
|
|
970
|
+
if (error instanceof NotSupportedError) {
|
|
971
|
+
throw this.validationError(
|
|
972
|
+
this.toDynamoExpressionValidationMessage(expressionType, error)
|
|
973
|
+
)
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
throw error
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
private toDynamoExpressionValidationMessage(
|
|
981
|
+
expressionType:
|
|
982
|
+
| "ConditionExpression"
|
|
983
|
+
| "FilterExpression"
|
|
984
|
+
| "KeyConditionExpression"
|
|
985
|
+
| "UpdateExpression",
|
|
986
|
+
error: NotSupportedError
|
|
987
|
+
): string {
|
|
988
|
+
const reason = String(error.reason ?? "").replace(/\.$/, "")
|
|
989
|
+
|
|
990
|
+
const namePlaceholder = error.featurePath.match(
|
|
991
|
+
/ExpressionAttributeNames\.(#[A-Za-z_][A-Za-z0-9_]*)$/
|
|
992
|
+
)?.[1]
|
|
993
|
+
if (namePlaceholder) {
|
|
994
|
+
return `Invalid ${expressionType}: An expression attribute name used in the document path is not defined; attribute name: ${namePlaceholder}`
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const valuePlaceholder = error.featurePath.match(
|
|
998
|
+
/ExpressionAttributeValues\.(:[A-Za-z_][A-Za-z0-9_]*)$/
|
|
999
|
+
)?.[1]
|
|
1000
|
+
if (
|
|
1001
|
+
reason === "ExpressionAttributeValues are required for value placeholders" ||
|
|
1002
|
+
reason === "Missing expression attribute value placeholder"
|
|
1003
|
+
) {
|
|
1004
|
+
const token = valuePlaceholder ?? ":missing"
|
|
1005
|
+
return `Invalid ${expressionType}: An expression attribute value used in expression is not defined; attribute value: ${token}`
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (
|
|
1009
|
+
reason === "Malformed SET assignment" ||
|
|
1010
|
+
reason === "Malformed REMOVE assignment"
|
|
1011
|
+
) {
|
|
1012
|
+
const token = reason.includes("REMOVE") ? "REMOVE" : "SET"
|
|
1013
|
+
return `Invalid UpdateExpression: Syntax error; token: "<EOF>", near: "${token}"`
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const invalidFunctionFromClause = reason.match(
|
|
1017
|
+
/^Unsupported clause:\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(/
|
|
1018
|
+
)?.[1]
|
|
1019
|
+
if (invalidFunctionFromClause) {
|
|
1020
|
+
return `Invalid ${expressionType}: Invalid function name; function: ${invalidFunctionFromClause}`
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const unsupportedValue = reason.match(/^Unsupported value token:\s*(.+)$/)?.[1]
|
|
1024
|
+
if (unsupportedValue) {
|
|
1025
|
+
const fn =
|
|
1026
|
+
unsupportedValue.match(/\band\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/i)?.[1] ??
|
|
1027
|
+
unsupportedValue.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*\(/)?.[1]
|
|
1028
|
+
if (fn) {
|
|
1029
|
+
return `Invalid ${expressionType}: Invalid function name; function: ${fn}`
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return reason
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
private assertExpressionAttributeInputs(
|
|
1037
|
+
params: AnyParams,
|
|
1038
|
+
expressions: Array<string | undefined>
|
|
1039
|
+
) {
|
|
1040
|
+
const activeExpressions = expressions.filter(
|
|
1041
|
+
(value): value is string => typeof value === "string" && value.trim().length > 0
|
|
1042
|
+
)
|
|
1043
|
+
if (activeExpressions.length === 0) return
|
|
1044
|
+
|
|
1045
|
+
if (typeof params.ExpressionAttributeValues !== "undefined") {
|
|
1046
|
+
const values = params.ExpressionAttributeValues
|
|
1047
|
+
if (!values || typeof values !== "object" || Array.isArray(values)) {
|
|
1048
|
+
throw this.validationError("ExpressionAttributeValues must not be empty")
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const valueKeys = Object.keys(values)
|
|
1052
|
+
if (valueKeys.length === 0) {
|
|
1053
|
+
throw this.validationError("ExpressionAttributeValues must not be empty")
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const usedValueTokens = new Set<string>()
|
|
1057
|
+
for (const expression of activeExpressions) {
|
|
1058
|
+
for (const token of expression.match(/:[A-Za-z_][A-Za-z0-9_]*/g) ?? []) {
|
|
1059
|
+
usedValueTokens.add(token)
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const unusedValues = valueKeys
|
|
1064
|
+
.filter((token) => !usedValueTokens.has(token))
|
|
1065
|
+
.sort()
|
|
1066
|
+
if (unusedValues.length > 0) {
|
|
1067
|
+
throw this.validationError(
|
|
1068
|
+
`Value provided in ExpressionAttributeValues unused in expressions: keys: {${unusedValues.join(", ")}}`
|
|
1069
|
+
)
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (typeof params.ExpressionAttributeNames !== "undefined") {
|
|
1074
|
+
const names = params.ExpressionAttributeNames
|
|
1075
|
+
if (!names || typeof names !== "object" || Array.isArray(names)) {
|
|
1076
|
+
throw this.validationError("ExpressionAttributeNames must not be empty")
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const nameKeys = Object.keys(names)
|
|
1080
|
+
if (nameKeys.length === 0) {
|
|
1081
|
+
throw this.validationError("ExpressionAttributeNames must not be empty")
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const usedNameTokens = new Set<string>()
|
|
1085
|
+
for (const expression of activeExpressions) {
|
|
1086
|
+
for (const token of expression.match(/#[A-Za-z_][A-Za-z0-9_]*/g) ?? []) {
|
|
1087
|
+
usedNameTokens.add(token)
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const unusedNames = nameKeys
|
|
1092
|
+
.filter((token) => !usedNameTokens.has(token))
|
|
1093
|
+
.sort()
|
|
1094
|
+
if (unusedNames.length > 0) {
|
|
1095
|
+
throw this.validationError(
|
|
1096
|
+
`Value provided in ExpressionAttributeNames unused in expressions: keys: {${unusedNames.join(", ")}}`
|
|
1097
|
+
)
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const createUnsupportedRequest = (
|
|
1104
|
+
method: string
|
|
1105
|
+
): ((params: AnyParams) => PromiseRequest<never>) => {
|
|
1106
|
+
return (_params: AnyParams) => ({
|
|
1107
|
+
promise: async () => {
|
|
1108
|
+
throw new NotSupportedError({
|
|
1109
|
+
method,
|
|
1110
|
+
featurePath: method,
|
|
1111
|
+
reason: `${method} is not supported in in-memory mode.`,
|
|
1112
|
+
})
|
|
1113
|
+
},
|
|
1114
|
+
})
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
export const createInMemoryDocumentClient = (): InMemoryDocumentClient => {
|
|
1118
|
+
const instance = new InMemoryDocumentClientImpl()
|
|
1119
|
+
|
|
1120
|
+
const proxied = new Proxy(instance as any, {
|
|
1121
|
+
get(target, prop, receiver) {
|
|
1122
|
+
if (typeof prop === "string") {
|
|
1123
|
+
if (prop in target) {
|
|
1124
|
+
const value = Reflect.get(target, prop, receiver)
|
|
1125
|
+
return typeof value === "function" ? value.bind(target) : value
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (IN_MEMORY_SPEC.unsupportedMethods.includes(prop)) {
|
|
1129
|
+
return createUnsupportedRequest(prop)
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return createUnsupportedRequest(prop)
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return Reflect.get(target, prop, receiver)
|
|
1136
|
+
},
|
|
1137
|
+
})
|
|
1138
|
+
|
|
1139
|
+
return proxied as InMemoryDocumentClient
|
|
1140
|
+
}
|