@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.
Files changed (91) hide show
  1. package/CHANGELOG.md +6 -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 +185 -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 +585 -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 +183 -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 +577 -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 +230 -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 +730 -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,2042 @@
1
+ import { Client } from "../client"
2
+ import { createSandbox, Sandbox } from "../sandbox"
3
+ import { IN_MEMORY_SPEC } from "../in-memory/spec"
4
+ import DynamoDB from "aws-sdk/clients/dynamodb"
5
+
6
+ type Engine = "local" | "memory"
7
+
8
+ type Context = {
9
+ engine: Engine
10
+ client: Client
11
+ sandbox: Sandbox
12
+ tableName: string
13
+ }
14
+
15
+ type NormalizedSuccess = {
16
+ ok: true
17
+ value: any
18
+ snapshot: { [key: string]: any }
19
+ }
20
+
21
+ type NormalizedError = {
22
+ ok: false
23
+ error: {
24
+ code: string
25
+ message?: string
26
+ method?: string
27
+ featurePath?: string
28
+ reason?: string
29
+ }
30
+ snapshot: { [key: string]: any }
31
+ }
32
+
33
+ type NormalizedOutcome = NormalizedSuccess | NormalizedError
34
+
35
+ type Vector = {
36
+ id: string
37
+ method: keyof typeof IN_MEMORY_SPEC.methods
38
+ setup?: (ctx: Context) => Promise<void>
39
+ execute: (ctx: Context) => Promise<any>
40
+ normalizeResult?: (value: any) => any
41
+ coverage?: {
42
+ supported?: string[]
43
+ unsupported?: string[]
44
+ }
45
+ }
46
+
47
+ const LOCAL_DDB = new DynamoDB({
48
+ accessKeyId: "xxx",
49
+ secretAccessKey: "xxx",
50
+ endpoint: process.env.LOCAL_ENDPOINT,
51
+ region: "local",
52
+ })
53
+
54
+ const withEngine = async <T>(engine: Engine, run: (ctx: Context) => Promise<T>) => {
55
+ const previous = process.env.EXPERIMENTAL_DYNAMODB_IN_MEMORY
56
+
57
+ if (engine === "memory") process.env.EXPERIMENTAL_DYNAMODB_IN_MEMORY = "1"
58
+ else delete process.env.EXPERIMENTAL_DYNAMODB_IN_MEMORY
59
+
60
+ const client = new Client({ tableName: "table" })
61
+ const sandbox = await createSandbox(client)
62
+
63
+ try {
64
+ return await run({
65
+ engine,
66
+ client,
67
+ sandbox,
68
+ tableName: client.tableName,
69
+ })
70
+ } finally {
71
+ await sandbox.destroy()
72
+
73
+ if (typeof previous === "undefined")
74
+ delete process.env.EXPERIMENTAL_DYNAMODB_IN_MEMORY
75
+ else process.env.EXPERIMENTAL_DYNAMODB_IN_MEMORY = previous
76
+ }
77
+ }
78
+
79
+ const normalizeScalar = (value: any): any => {
80
+ if (typeof value === "string") {
81
+ return value.replace(/[0-9a-f]{40}/gi, "<table>")
82
+ }
83
+
84
+ return value
85
+ }
86
+
87
+ const normalizeObject = (value: any): any => {
88
+ if (value === undefined) return undefined
89
+ if (Array.isArray(value)) return value.map(normalizeObject)
90
+
91
+ if (value && typeof value === "object") {
92
+ return Object.fromEntries(
93
+ Object.keys(value)
94
+ .sort()
95
+ .filter((key) => typeof value[key] !== "undefined")
96
+ .map((key) => [normalizeScalar(key), normalizeObject(value[key])])
97
+ )
98
+ }
99
+
100
+ return normalizeScalar(value)
101
+ }
102
+
103
+ const normalizeSnapshot = (snapshot: { [key: string]: any }) =>
104
+ Object.fromEntries(
105
+ Object.keys(snapshot)
106
+ .sort()
107
+ .map((key) => [key, normalizeObject(snapshot[key])])
108
+ )
109
+
110
+ const sanitizeErrorMessage = (message: string): string =>
111
+ canonicalizeValidationMessage(
112
+ message
113
+ .replace(/[0-9a-f]{40}/gi, "<table>")
114
+ .replace(/\s+/g, " ")
115
+ .trim()
116
+ .replace(/\.$/, "")
117
+ .replace(/\([^)]*\)/g, (match) => {
118
+ if (match.includes("<table>")) return "(<table>)"
119
+ return match
120
+ })
121
+ )
122
+
123
+ const canonicalizeValidationMessage = (message: string): string => {
124
+ if (
125
+ message ===
126
+ "Consistent read cannot be true when querying a GSI" ||
127
+ message ===
128
+ "Consistent reads are not supported on global secondary indexes"
129
+ ) {
130
+ return "Consistent reads are not supported on global secondary indexes"
131
+ }
132
+
133
+ if (
134
+ message === "The provided starting key is invalid" ||
135
+ message === "Exclusive Start Key must have same size as table's key schema"
136
+ ) {
137
+ return "The provided starting key is invalid"
138
+ }
139
+
140
+ return message
141
+ }
142
+
143
+ const normalizeError = (error: any) => {
144
+ const code =
145
+ error?.code ??
146
+ (typeof error?.name === "string" && error.name !== "Error"
147
+ ? error.name
148
+ : "UnknownError")
149
+
150
+ if (code === "NotSupportedError") {
151
+ return {
152
+ code,
153
+ message: sanitizeErrorMessage(String(error?.message ?? "")),
154
+ method: error?.method,
155
+ featurePath: error?.featurePath,
156
+ reason: error?.reason,
157
+ }
158
+ }
159
+
160
+ return {
161
+ code,
162
+ message: sanitizeErrorMessage(String(error?.message ?? "")),
163
+ }
164
+ }
165
+
166
+ const normalizeResult = (method: Vector["method"], value: any): any => {
167
+ if (!value || typeof value !== "object") return value
168
+
169
+ if (method === "batchGet") {
170
+ const responses = value.Responses ?? {}
171
+ const normalizedResponses = Object.fromEntries(
172
+ Object.keys(responses)
173
+ .sort()
174
+ .map((tableName) => [
175
+ normalizeScalar(tableName),
176
+ [...responses[tableName]].sort(compareItemsByPKSK).map(normalizeObject),
177
+ ])
178
+ )
179
+
180
+ const unprocessed = value.UnprocessedKeys ?? value.UnprocessedItems ?? {}
181
+ const normalizedUnprocessed = Object.fromEntries(
182
+ Object.keys(unprocessed)
183
+ .sort()
184
+ .map((tableName) => [normalizeScalar(tableName), normalizeObject(unprocessed[tableName])])
185
+ )
186
+
187
+ return { Responses: normalizedResponses, Unprocessed: normalizedUnprocessed }
188
+ }
189
+
190
+ if (method === "delete") {
191
+ const normalized = normalizeObject(value) ?? {}
192
+ if (!normalized || typeof normalized !== "object") return normalized
193
+ const { ConsumedCapacity: _ignored, ...rest } = normalized
194
+ return rest
195
+ }
196
+
197
+ if (method === "scan") {
198
+ return {
199
+ ...normalizeObject(value),
200
+ Items: [...(value.Items ?? [])].sort(compareItemsByPKSK).map(normalizeObject),
201
+ }
202
+ }
203
+
204
+ return normalizeObject(value)
205
+ }
206
+
207
+ const compareItemsByPKSK = (left: any, right: any) => {
208
+ const leftPK = String(left?.PK ?? "")
209
+ const rightPK = String(right?.PK ?? "")
210
+ if (leftPK !== rightPK) return leftPK < rightPK ? -1 : 1
211
+
212
+ const leftSK = String(left?.SK ?? "")
213
+ const rightSK = String(right?.SK ?? "")
214
+ if (leftSK !== rightSK) return leftSK < rightSK ? -1 : 1
215
+
216
+ return 0
217
+ }
218
+
219
+ const runVector = async (engine: Engine, vector: Vector): Promise<NormalizedOutcome> => {
220
+ return withEngine(engine, async (ctx) => {
221
+ if (vector.setup) {
222
+ await vector.setup(ctx)
223
+ }
224
+
225
+ try {
226
+ const raw = await vector.execute(ctx)
227
+ const snapshot = normalizeSnapshot(await ctx.sandbox.snapshot())
228
+
229
+ return {
230
+ ok: true,
231
+ value: vector.normalizeResult
232
+ ? vector.normalizeResult(raw)
233
+ : normalizeResult(vector.method, raw),
234
+ snapshot,
235
+ }
236
+ } catch (error) {
237
+ const snapshot = normalizeSnapshot(await ctx.sandbox.snapshot())
238
+
239
+ return {
240
+ ok: false,
241
+ error: normalizeError(error),
242
+ snapshot,
243
+ }
244
+ }
245
+ })
246
+ }
247
+
248
+ const createSeed = async (ctx: Context) => {
249
+ await ctx.client.documentClient
250
+ .batchWrite({
251
+ RequestItems: {
252
+ [ctx.tableName]: [
253
+ {
254
+ PutRequest: {
255
+ Item: {
256
+ PK: "USER#1",
257
+ SK: "PROFILE#001",
258
+ GSI2PK: "EMAIL#ada@example.com",
259
+ GSI2SK: "USER#1",
260
+ name: "Ada",
261
+ age: 30,
262
+ status: "active",
263
+ score: 10,
264
+ tags: ["math", "history"],
265
+ },
266
+ },
267
+ },
268
+ {
269
+ PutRequest: {
270
+ Item: {
271
+ PK: "USER#1",
272
+ SK: "ORDER#001",
273
+ GSI2PK: "ORDER#OPEN",
274
+ GSI2SK: "2021-01-01T00:00:00.000Z",
275
+ total: 120,
276
+ currency: "USD",
277
+ status: "open",
278
+ },
279
+ },
280
+ },
281
+ {
282
+ PutRequest: {
283
+ Item: {
284
+ PK: "USER#1",
285
+ SK: "ORDER#002",
286
+ GSI2PK: "ORDER#CLOSED",
287
+ GSI2SK: "2021-01-02T00:00:00.000Z",
288
+ total: 99,
289
+ currency: "USD",
290
+ status: "closed",
291
+ },
292
+ },
293
+ },
294
+ {
295
+ PutRequest: {
296
+ Item: {
297
+ PK: "USER#2",
298
+ SK: "PROFILE#001",
299
+ GSI2PK: "EMAIL#grace@example.com",
300
+ GSI2SK: "USER#2",
301
+ name: "Grace",
302
+ age: 35,
303
+ status: "pending",
304
+ score: 20,
305
+ },
306
+ },
307
+ },
308
+ {
309
+ PutRequest: {
310
+ Item: {
311
+ PK: "USER#3",
312
+ SK: "PROFILE#001",
313
+ GSI2PK: "EMAIL#alan@example.com",
314
+ GSI2SK: "USER#3",
315
+ name: "Alan",
316
+ age: 28,
317
+ status: "active",
318
+ score: 15,
319
+ },
320
+ },
321
+ },
322
+ ],
323
+ },
324
+ })
325
+ .promise()
326
+ }
327
+
328
+ const createAuxTableIfNeeded = async (ctx: Context, tableName: string) => {
329
+ if (ctx.engine !== "local") return
330
+
331
+ await LOCAL_DDB.createTable({
332
+ TableName: tableName,
333
+ AttributeDefinitions: [
334
+ { AttributeName: "PK", AttributeType: "S" },
335
+ { AttributeName: "SK", AttributeType: "S" },
336
+ ],
337
+ KeySchema: [
338
+ { AttributeName: "PK", KeyType: "HASH" },
339
+ { AttributeName: "SK", KeyType: "RANGE" },
340
+ ],
341
+ BillingMode: "PAY_PER_REQUEST",
342
+ })
343
+ .promise()
344
+ .catch((error: any) => {
345
+ if (error?.code === "ResourceInUseException") return
346
+ throw error
347
+ })
348
+
349
+ await LOCAL_DDB.waitFor("tableExists", { TableName: tableName }).promise()
350
+ }
351
+
352
+ const destroyAuxTableIfNeeded = async (ctx: Context, tableName: string) => {
353
+ if (ctx.engine !== "local") return
354
+
355
+ await LOCAL_DDB.deleteTable({ TableName: tableName })
356
+ .promise()
357
+ .catch((error: any) => {
358
+ if (error?.code === "ResourceNotFoundException") return
359
+ throw error
360
+ })
361
+
362
+ await LOCAL_DDB.waitFor("tableNotExists", { TableName: tableName })
363
+ .promise()
364
+ .catch((error: any) => {
365
+ if (error?.code === "ResourceNotFoundException") return
366
+ throw error
367
+ })
368
+ }
369
+
370
+ const baseVectorsByMethod: {
371
+ [K in keyof typeof IN_MEMORY_SPEC.methods]: Vector[]
372
+ } = {
373
+ get: [
374
+ {
375
+ id: "get.existing",
376
+ method: "get",
377
+ setup: createSeed,
378
+ execute: ({ client, tableName }) =>
379
+ client.documentClient
380
+ .get({ TableName: tableName, Key: { PK: "USER#1", SK: "PROFILE#001" } })
381
+ .promise(),
382
+ },
383
+ {
384
+ id: "get.missing",
385
+ method: "get",
386
+ setup: createSeed,
387
+ execute: ({ client, tableName }) =>
388
+ client.documentClient
389
+ .get({ TableName: tableName, Key: { PK: "USER#404", SK: "PROFILE#001" } })
390
+ .promise(),
391
+ },
392
+ {
393
+ id: "get.consistent-read",
394
+ method: "get",
395
+ setup: createSeed,
396
+ execute: ({ client, tableName }) =>
397
+ client.documentClient
398
+ .get({
399
+ TableName: tableName,
400
+ Key: { PK: "USER#2", SK: "PROFILE#001" },
401
+ ConsistentRead: true,
402
+ })
403
+ .promise(),
404
+ },
405
+ {
406
+ id: "get.bad-key",
407
+ method: "get",
408
+ execute: ({ client, tableName }) =>
409
+ client.documentClient
410
+ .get({ TableName: tableName, Key: { PK: 123, SK: "A" } as any })
411
+ .promise(),
412
+ },
413
+ ],
414
+ put: [
415
+ {
416
+ id: "put.insert",
417
+ method: "put",
418
+ execute: ({ client, tableName }) =>
419
+ client.documentClient
420
+ .put({
421
+ TableName: tableName,
422
+ Item: {
423
+ PK: "USER#10",
424
+ SK: "PROFILE#001",
425
+ GSI2PK: "EMAIL#new@example.com",
426
+ GSI2SK: "USER#10",
427
+ name: "New",
428
+ score: 1,
429
+ },
430
+ })
431
+ .promise(),
432
+ },
433
+ {
434
+ id: "put.overwrite",
435
+ method: "put",
436
+ setup: createSeed,
437
+ execute: ({ client, tableName }) =>
438
+ client.documentClient
439
+ .put({
440
+ TableName: tableName,
441
+ Item: {
442
+ PK: "USER#1",
443
+ SK: "PROFILE#001",
444
+ GSI2PK: "EMAIL#ada@example.com",
445
+ GSI2SK: "USER#1",
446
+ name: "Ada Lovelace",
447
+ age: 31,
448
+ },
449
+ })
450
+ .promise(),
451
+ },
452
+ {
453
+ id: "put.conditional-fail",
454
+ method: "put",
455
+ setup: createSeed,
456
+ execute: ({ client, tableName }) =>
457
+ client.documentClient
458
+ .put({
459
+ TableName: tableName,
460
+ Item: {
461
+ PK: "USER#1",
462
+ SK: "PROFILE#001",
463
+ name: "Nope",
464
+ },
465
+ ConditionExpression: "attribute_not_exists(PK)",
466
+ })
467
+ .promise(),
468
+ },
469
+ {
470
+ id: "put.conditional-pass-with-placeholders",
471
+ method: "put",
472
+ setup: createSeed,
473
+ execute: ({ client, tableName }) =>
474
+ client.documentClient
475
+ .put({
476
+ TableName: tableName,
477
+ Item: {
478
+ PK: "USER#11",
479
+ SK: "PROFILE#001",
480
+ name: "Placeholder",
481
+ status: "active",
482
+ },
483
+ ConditionExpression: "attribute_not_exists(#pk) and :status = :status",
484
+ ExpressionAttributeNames: { "#pk": "PK" },
485
+ ExpressionAttributeValues: { ":status": "active" },
486
+ })
487
+ .promise(),
488
+ },
489
+ {
490
+ id: "put.bad-condition-expression",
491
+ method: "put",
492
+ execute: ({ client, tableName }) =>
493
+ client.documentClient
494
+ .put({
495
+ TableName: tableName,
496
+ Item: { PK: "A", SK: "A" },
497
+ ConditionExpression: "unknown_fn(PK)",
498
+ })
499
+ .promise(),
500
+ },
501
+ ],
502
+ update: [
503
+ {
504
+ id: "update.set-and-return-all-new",
505
+ method: "update",
506
+ setup: createSeed,
507
+ execute: ({ client, tableName }) =>
508
+ client.documentClient
509
+ .update({
510
+ TableName: tableName,
511
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
512
+ UpdateExpression: "SET #name = :name, #score = :score",
513
+ ExpressionAttributeNames: { "#name": "name", "#score": "score" },
514
+ ExpressionAttributeValues: { ":name": "Ada Updated", ":score": 11 },
515
+ ReturnValues: "ALL_NEW",
516
+ })
517
+ .promise(),
518
+ },
519
+ {
520
+ id: "update.remove-gsi",
521
+ method: "update",
522
+ setup: createSeed,
523
+ execute: ({ client, tableName }) =>
524
+ client.documentClient
525
+ .update({
526
+ TableName: tableName,
527
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
528
+ UpdateExpression: "REMOVE GSI2PK, GSI2SK",
529
+ ReturnValues: "ALL_NEW",
530
+ })
531
+ .promise(),
532
+ },
533
+ {
534
+ id: "update.upsert",
535
+ method: "update",
536
+ execute: ({ client, tableName }) =>
537
+ client.documentClient
538
+ .update({
539
+ TableName: tableName,
540
+ Key: { PK: "USER#99", SK: "PROFILE#001" },
541
+ UpdateExpression: "SET #name = :name",
542
+ ExpressionAttributeNames: { "#name": "name" },
543
+ ExpressionAttributeValues: { ":name": "Created" },
544
+ ReturnValues: "ALL_NEW",
545
+ })
546
+ .promise(),
547
+ },
548
+ {
549
+ id: "update.conditional-fail",
550
+ method: "update",
551
+ setup: createSeed,
552
+ execute: ({ client, tableName }) =>
553
+ client.documentClient
554
+ .update({
555
+ TableName: tableName,
556
+ Key: { PK: "USER#404", SK: "PROFILE#001" },
557
+ ConditionExpression: "attribute_exists(PK)",
558
+ UpdateExpression: "SET #name = :name",
559
+ ExpressionAttributeNames: { "#name": "name" },
560
+ ExpressionAttributeValues: { ":name": "Nope" },
561
+ })
562
+ .promise(),
563
+ },
564
+ {
565
+ id: "update.bad-update-expression",
566
+ method: "update",
567
+ setup: createSeed,
568
+ execute: ({ client, tableName }) =>
569
+ client.documentClient
570
+ .update({
571
+ TableName: tableName,
572
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
573
+ UpdateExpression: "SET",
574
+ })
575
+ .promise(),
576
+ },
577
+ {
578
+ id: "update.missing-placeholder",
579
+ method: "update",
580
+ setup: createSeed,
581
+ execute: ({ client, tableName }) =>
582
+ client.documentClient
583
+ .update({
584
+ TableName: tableName,
585
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
586
+ UpdateExpression: "SET #name = :missing",
587
+ ExpressionAttributeNames: { "#name": "name" },
588
+ })
589
+ .promise(),
590
+ },
591
+ {
592
+ id: "update.key-mutation-error",
593
+ method: "update",
594
+ setup: createSeed,
595
+ execute: ({ client, tableName }) =>
596
+ client.documentClient
597
+ .update({
598
+ TableName: tableName,
599
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
600
+ UpdateExpression: "SET PK = :pk",
601
+ ExpressionAttributeValues: { ":pk": "USER#X" },
602
+ })
603
+ .promise(),
604
+ },
605
+ ],
606
+ delete: [
607
+ {
608
+ id: "delete.existing",
609
+ method: "delete",
610
+ setup: createSeed,
611
+ execute: ({ client, tableName }) =>
612
+ client.documentClient
613
+ .delete({ TableName: tableName, Key: { PK: "USER#3", SK: "PROFILE#001" } })
614
+ .promise(),
615
+ },
616
+ {
617
+ id: "delete.missing",
618
+ method: "delete",
619
+ setup: createSeed,
620
+ execute: ({ client, tableName }) =>
621
+ client.documentClient
622
+ .delete({ TableName: tableName, Key: { PK: "USER#404", SK: "PROFILE#001" } })
623
+ .promise(),
624
+ },
625
+ {
626
+ id: "delete.conditional-fail",
627
+ method: "delete",
628
+ setup: createSeed,
629
+ execute: ({ client, tableName }) =>
630
+ client.documentClient
631
+ .delete({
632
+ TableName: tableName,
633
+ Key: { PK: "USER#2", SK: "PROFILE#001" },
634
+ ConditionExpression: "#status = :status",
635
+ ExpressionAttributeNames: { "#status": "status" },
636
+ ExpressionAttributeValues: { ":status": "active" },
637
+ })
638
+ .promise(),
639
+ },
640
+ {
641
+ id: "delete.conditional-pass",
642
+ method: "delete",
643
+ setup: createSeed,
644
+ execute: ({ client, tableName }) =>
645
+ client.documentClient
646
+ .delete({
647
+ TableName: tableName,
648
+ Key: { PK: "USER#2", SK: "PROFILE#001" },
649
+ ConditionExpression: "#status = :status",
650
+ ExpressionAttributeNames: { "#status": "status" },
651
+ ExpressionAttributeValues: { ":status": "pending" },
652
+ })
653
+ .promise(),
654
+ },
655
+ ],
656
+ query: [
657
+ {
658
+ id: "query.hash-only",
659
+ method: "query",
660
+ setup: createSeed,
661
+ execute: ({ client, tableName }) =>
662
+ client.documentClient
663
+ .query({
664
+ TableName: tableName,
665
+ KeyConditionExpression: "PK = :pk",
666
+ ExpressionAttributeValues: { ":pk": "USER#1" },
667
+ })
668
+ .promise(),
669
+ },
670
+ {
671
+ id: "query.begins-with",
672
+ method: "query",
673
+ setup: createSeed,
674
+ execute: ({ client, tableName }) =>
675
+ client.documentClient
676
+ .query({
677
+ TableName: tableName,
678
+ KeyConditionExpression: "PK = :pk and begins_with(SK, :prefix)",
679
+ ExpressionAttributeValues: {
680
+ ":pk": "USER#1",
681
+ ":prefix": "ORDER#",
682
+ },
683
+ })
684
+ .promise(),
685
+ },
686
+ {
687
+ id: "query.range-between",
688
+ method: "query",
689
+ setup: createSeed,
690
+ execute: ({ client, tableName }) =>
691
+ client.documentClient
692
+ .query({
693
+ TableName: tableName,
694
+ KeyConditionExpression: "PK = :pk and SK between :from and :to",
695
+ ExpressionAttributeValues: {
696
+ ":pk": "USER#1",
697
+ ":from": "ORDER#001",
698
+ ":to": "ORDER#010",
699
+ },
700
+ })
701
+ .promise(),
702
+ },
703
+ {
704
+ id: "query.range-operator-and-backward",
705
+ method: "query",
706
+ setup: createSeed,
707
+ execute: ({ client, tableName }) =>
708
+ client.documentClient
709
+ .query({
710
+ TableName: tableName,
711
+ KeyConditionExpression: "PK = :pk and SK >= :from",
712
+ ExpressionAttributeValues: {
713
+ ":pk": "USER#1",
714
+ ":from": "ORDER#001",
715
+ },
716
+ ScanIndexForward: false,
717
+ })
718
+ .promise(),
719
+ },
720
+ {
721
+ id: "query.filter-limit-scanned-count",
722
+ method: "query",
723
+ setup: createSeed,
724
+ execute: ({ client, tableName }) =>
725
+ client.documentClient
726
+ .query({
727
+ TableName: tableName,
728
+ KeyConditionExpression: "PK = :pk",
729
+ FilterExpression: "#status = :status",
730
+ ExpressionAttributeNames: { "#status": "status" },
731
+ ExpressionAttributeValues: {
732
+ ":pk": "USER#1",
733
+ ":status": "open",
734
+ },
735
+ Limit: 1,
736
+ })
737
+ .promise(),
738
+ },
739
+ {
740
+ id: "query.pagination-exclusive-start",
741
+ method: "query",
742
+ setup: async (ctx) => {
743
+ await createSeed(ctx)
744
+ },
745
+ execute: ({ client, tableName }) =>
746
+ client.documentClient
747
+ .query({
748
+ TableName: tableName,
749
+ KeyConditionExpression: "PK = :pk",
750
+ ExpressionAttributeValues: { ":pk": "USER#1" },
751
+ Limit: 1,
752
+ })
753
+ .promise(),
754
+ },
755
+ {
756
+ id: "query.gsi",
757
+ method: "query",
758
+ setup: createSeed,
759
+ execute: ({ client, tableName }) =>
760
+ client.documentClient
761
+ .query({
762
+ TableName: tableName,
763
+ IndexName: "GSI2",
764
+ KeyConditionExpression: "GSI2PK = :pk",
765
+ ExpressionAttributeValues: { ":pk": "ORDER#OPEN" },
766
+ })
767
+ .promise(),
768
+ },
769
+ {
770
+ id: "query.gsi-consistent-read-error",
771
+ method: "query",
772
+ setup: createSeed,
773
+ execute: ({ client, tableName }) =>
774
+ client.documentClient
775
+ .query({
776
+ TableName: tableName,
777
+ IndexName: "GSI2",
778
+ KeyConditionExpression: "GSI2PK = :pk",
779
+ ExpressionAttributeValues: { ":pk": "ORDER#OPEN" },
780
+ ConsistentRead: true,
781
+ })
782
+ .promise(),
783
+ },
784
+ {
785
+ id: "query.bad-key-condition",
786
+ method: "query",
787
+ setup: createSeed,
788
+ execute: ({ client, tableName }) =>
789
+ client.documentClient
790
+ .query({
791
+ TableName: tableName,
792
+ KeyConditionExpression: "PK = :pk and invalid(SK, :x)",
793
+ ExpressionAttributeValues: {
794
+ ":pk": "USER#1",
795
+ ":x": "A",
796
+ },
797
+ })
798
+ .promise(),
799
+ },
800
+ ],
801
+ scan: [
802
+ {
803
+ id: "scan.all",
804
+ method: "scan",
805
+ setup: createSeed,
806
+ execute: ({ client, tableName }) =>
807
+ client.documentClient.scan({ TableName: tableName }).promise(),
808
+ },
809
+ {
810
+ id: "scan.filter-limit",
811
+ method: "scan",
812
+ setup: createSeed,
813
+ execute: ({ client, tableName }) =>
814
+ client.documentClient
815
+ .scan({
816
+ TableName: tableName,
817
+ FilterExpression: "attribute_exists(GSI2PK) and #status = :status",
818
+ ExpressionAttributeNames: { "#status": "status" },
819
+ ExpressionAttributeValues: { ":status": "active" },
820
+ })
821
+ .promise(),
822
+ },
823
+ {
824
+ id: "scan.bad-limit",
825
+ method: "scan",
826
+ setup: createSeed,
827
+ execute: ({ client, tableName }) =>
828
+ client.documentClient
829
+ .scan({ TableName: tableName, Limit: 0 })
830
+ .promise(),
831
+ },
832
+ ],
833
+ batchGet: [
834
+ {
835
+ id: "batch-get.basic",
836
+ method: "batchGet",
837
+ setup: createSeed,
838
+ execute: ({ client, tableName }) =>
839
+ client.documentClient
840
+ .batchGet({
841
+ RequestItems: {
842
+ [tableName]: {
843
+ Keys: [
844
+ { PK: "USER#1", SK: "PROFILE#001" },
845
+ { PK: "USER#2", SK: "PROFILE#001" },
846
+ ],
847
+ },
848
+ },
849
+ })
850
+ .promise(),
851
+ },
852
+ {
853
+ id: "batch-get.missing-items",
854
+ method: "batchGet",
855
+ setup: createSeed,
856
+ execute: ({ client, tableName }) =>
857
+ client.documentClient
858
+ .batchGet({
859
+ RequestItems: {
860
+ [tableName]: {
861
+ Keys: [
862
+ { PK: "USER#1", SK: "PROFILE#001" },
863
+ { PK: "USER#404", SK: "PROFILE#001" },
864
+ ],
865
+ },
866
+ },
867
+ })
868
+ .promise(),
869
+ },
870
+ {
871
+ id: "batch-get.duplicate-keys-error",
872
+ method: "batchGet",
873
+ setup: createSeed,
874
+ execute: ({ client, tableName }) =>
875
+ client.documentClient
876
+ .batchGet({
877
+ RequestItems: {
878
+ [tableName]: {
879
+ Keys: [
880
+ { PK: "USER#1", SK: "PROFILE#001" },
881
+ { PK: "USER#1", SK: "PROFILE#001" },
882
+ ],
883
+ },
884
+ },
885
+ })
886
+ .promise(),
887
+ },
888
+ {
889
+ id: "batch-get.too-many-keys-error",
890
+ method: "batchGet",
891
+ execute: ({ client, tableName }) =>
892
+ client.documentClient
893
+ .batchGet({
894
+ RequestItems: {
895
+ [tableName]: {
896
+ Keys: Array.from({ length: 101 }).map((_, i) => ({
897
+ PK: `USER#${i}`,
898
+ SK: "PROFILE#001",
899
+ })),
900
+ },
901
+ },
902
+ })
903
+ .promise(),
904
+ },
905
+ ],
906
+ batchWrite: [
907
+ {
908
+ id: "batch-write.put-and-delete",
909
+ method: "batchWrite",
910
+ setup: createSeed,
911
+ execute: ({ client, tableName }) =>
912
+ client.documentClient
913
+ .batchWrite({
914
+ RequestItems: {
915
+ [tableName]: [
916
+ {
917
+ PutRequest: {
918
+ Item: {
919
+ PK: "USER#20",
920
+ SK: "PROFILE#001",
921
+ name: "BatchPut",
922
+ },
923
+ },
924
+ },
925
+ {
926
+ DeleteRequest: {
927
+ Key: { PK: "USER#3", SK: "PROFILE#001" },
928
+ },
929
+ },
930
+ ],
931
+ },
932
+ })
933
+ .promise(),
934
+ },
935
+ {
936
+ id: "batch-write.too-many-items-error",
937
+ method: "batchWrite",
938
+ execute: ({ client, tableName }) =>
939
+ client.documentClient
940
+ .batchWrite({
941
+ RequestItems: {
942
+ [tableName]: Array.from({ length: 26 }).map((_, i) => ({
943
+ PutRequest: {
944
+ Item: { PK: `U#${i}`, SK: "S#1" },
945
+ },
946
+ })),
947
+ },
948
+ })
949
+ .promise(),
950
+ },
951
+ {
952
+ id: "batch-write.invalid-entry-error",
953
+ method: "batchWrite",
954
+ execute: ({ client, tableName }) =>
955
+ client.documentClient
956
+ .batchWrite({
957
+ RequestItems: {
958
+ [tableName]: [{ Nope: true } as any],
959
+ },
960
+ })
961
+ .promise(),
962
+ },
963
+ ],
964
+ transactWrite: [
965
+ {
966
+ id: "transact-write.success",
967
+ method: "transactWrite",
968
+ setup: createSeed,
969
+ execute: ({ client, tableName }) =>
970
+ client.documentClient
971
+ .transactWrite({
972
+ TransactItems: [
973
+ {
974
+ ConditionCheck: {
975
+ TableName: tableName,
976
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
977
+ ConditionExpression: "attribute_exists(PK)",
978
+ },
979
+ },
980
+ {
981
+ Update: {
982
+ TableName: tableName,
983
+ Key: { PK: "USER#2", SK: "PROFILE#001" },
984
+ UpdateExpression: "SET #score = :score",
985
+ ExpressionAttributeNames: { "#score": "score" },
986
+ ExpressionAttributeValues: { ":score": 25 },
987
+ },
988
+ },
989
+ {
990
+ Put: {
991
+ TableName: tableName,
992
+ Item: {
993
+ PK: "USER#30",
994
+ SK: "PROFILE#001",
995
+ name: "FromTx",
996
+ },
997
+ ConditionExpression: "attribute_not_exists(PK)",
998
+ },
999
+ },
1000
+ {
1001
+ Delete: {
1002
+ TableName: tableName,
1003
+ Key: { PK: "USER#3", SK: "PROFILE#001" },
1004
+ },
1005
+ },
1006
+ ],
1007
+ })
1008
+ .promise(),
1009
+ },
1010
+ {
1011
+ id: "transact-write.conditional-fail-and-rollback",
1012
+ method: "transactWrite",
1013
+ setup: createSeed,
1014
+ execute: ({ client, tableName }) =>
1015
+ client.documentClient
1016
+ .transactWrite({
1017
+ TransactItems: [
1018
+ {
1019
+ Update: {
1020
+ TableName: tableName,
1021
+ Key: { PK: "USER#2", SK: "PROFILE#001" },
1022
+ UpdateExpression: "SET #score = :score",
1023
+ ExpressionAttributeNames: { "#score": "score" },
1024
+ ExpressionAttributeValues: { ":score": 100 },
1025
+ },
1026
+ },
1027
+ {
1028
+ ConditionCheck: {
1029
+ TableName: tableName,
1030
+ Key: { PK: "USER#404", SK: "PROFILE#001" },
1031
+ ConditionExpression: "attribute_exists(PK)",
1032
+ },
1033
+ },
1034
+ ],
1035
+ })
1036
+ .promise(),
1037
+ },
1038
+ {
1039
+ id: "transact-write.duplicate-target-error",
1040
+ method: "transactWrite",
1041
+ setup: createSeed,
1042
+ execute: ({ client, tableName }) =>
1043
+ client.documentClient
1044
+ .transactWrite({
1045
+ TransactItems: [
1046
+ {
1047
+ Update: {
1048
+ TableName: tableName,
1049
+ Key: { PK: "USER#2", SK: "PROFILE#001" },
1050
+ UpdateExpression: "SET #score = :score",
1051
+ ExpressionAttributeNames: { "#score": "score" },
1052
+ ExpressionAttributeValues: { ":score": 100 },
1053
+ },
1054
+ },
1055
+ {
1056
+ Delete: {
1057
+ TableName: tableName,
1058
+ Key: { PK: "USER#2", SK: "PROFILE#001" },
1059
+ },
1060
+ },
1061
+ ],
1062
+ })
1063
+ .promise(),
1064
+ },
1065
+ {
1066
+ id: "transact-write.too-many-items-error",
1067
+ method: "transactWrite",
1068
+ execute: ({ client, tableName }) =>
1069
+ client.documentClient
1070
+ .transactWrite({
1071
+ TransactItems: Array.from({ length: 101 }).map((_, i) => ({
1072
+ Put: {
1073
+ TableName: tableName,
1074
+ Item: { PK: `TX#${i}`, SK: "S#1" },
1075
+ },
1076
+ })),
1077
+ })
1078
+ .promise(),
1079
+ },
1080
+ {
1081
+ id: "transact-write.bad-update-expression-error",
1082
+ method: "transactWrite",
1083
+ setup: createSeed,
1084
+ execute: ({ client, tableName }) =>
1085
+ client.documentClient
1086
+ .transactWrite({
1087
+ TransactItems: [
1088
+ {
1089
+ Update: {
1090
+ TableName: tableName,
1091
+ Key: { PK: "USER#2", SK: "PROFILE#001" },
1092
+ UpdateExpression: "SET",
1093
+ },
1094
+ },
1095
+ ],
1096
+ })
1097
+ .promise(),
1098
+ },
1099
+ ],
1100
+ }
1101
+
1102
+ const generatedVectors: Vector[] = Object.keys(IN_MEMORY_SPEC.methods).flatMap(
1103
+ (method) => {
1104
+ const typedMethod = method as keyof typeof IN_MEMORY_SPEC.methods
1105
+ return baseVectorsByMethod[typedMethod]
1106
+ }
1107
+ )
1108
+
1109
+ const additionalDifferentialVectors: Vector[] = [
1110
+ {
1111
+ id: "query.pagination-continuity",
1112
+ method: "query",
1113
+ setup: createSeed,
1114
+ execute: async ({ client, tableName }) => {
1115
+ const p1 = await client.documentClient
1116
+ .query({
1117
+ TableName: tableName,
1118
+ KeyConditionExpression: "PK = :pk",
1119
+ ExpressionAttributeValues: { ":pk": "USER#1" },
1120
+ Limit: 1,
1121
+ })
1122
+ .promise()
1123
+
1124
+ const p2 = await client.documentClient
1125
+ .query({
1126
+ TableName: tableName,
1127
+ KeyConditionExpression: "PK = :pk",
1128
+ ExpressionAttributeValues: { ":pk": "USER#1" },
1129
+ Limit: 1,
1130
+ ExclusiveStartKey: p1.LastEvaluatedKey,
1131
+ })
1132
+ .promise()
1133
+
1134
+ const p3 = await client.documentClient
1135
+ .query({
1136
+ TableName: tableName,
1137
+ KeyConditionExpression: "PK = :pk",
1138
+ ExpressionAttributeValues: { ":pk": "USER#1" },
1139
+ Limit: 10,
1140
+ ExclusiveStartKey: p2.LastEvaluatedKey,
1141
+ })
1142
+ .promise()
1143
+
1144
+ return { p1, p2, p3 }
1145
+ },
1146
+ },
1147
+ {
1148
+ id: "scan.pagination-continuity",
1149
+ method: "scan",
1150
+ setup: createSeed,
1151
+ execute: async ({ client, tableName }) => {
1152
+ const pages: any[] = []
1153
+ let startKey: any = undefined
1154
+
1155
+ do {
1156
+ const page = await client.documentClient
1157
+ .scan({
1158
+ TableName: tableName,
1159
+ Limit: 2,
1160
+ ExclusiveStartKey: startKey,
1161
+ })
1162
+ .promise()
1163
+
1164
+ pages.push(page)
1165
+ startKey = page.LastEvaluatedKey
1166
+ } while (startKey)
1167
+
1168
+ const full = await client.documentClient
1169
+ .scan({
1170
+ TableName: tableName,
1171
+ })
1172
+ .promise()
1173
+
1174
+ return {
1175
+ pagedItems: pages.flatMap((page) => page.Items ?? []),
1176
+ fullItems: full.Items ?? [],
1177
+ pageCount: pages.length,
1178
+ }
1179
+ },
1180
+ normalizeResult: (value) => ({
1181
+ ...normalizeObject(value),
1182
+ pagedItems: [...(value.pagedItems ?? [])]
1183
+ .sort(compareItemsByPKSK)
1184
+ .map(normalizeObject),
1185
+ fullItems: [...(value.fullItems ?? [])]
1186
+ .sort(compareItemsByPKSK)
1187
+ .map(normalizeObject),
1188
+ pageCount: value.pageCount,
1189
+ pagedMatchesFull:
1190
+ JSON.stringify(
1191
+ [...(value.pagedItems ?? [])].sort(compareItemsByPKSK).map(normalizeObject)
1192
+ ) ===
1193
+ JSON.stringify(
1194
+ [...(value.fullItems ?? [])].sort(compareItemsByPKSK).map(normalizeObject)
1195
+ ),
1196
+ }),
1197
+ },
1198
+ {
1199
+ id: "query.bad-exclusive-start-key",
1200
+ method: "query",
1201
+ setup: createSeed,
1202
+ execute: ({ client, tableName }) =>
1203
+ client.documentClient
1204
+ .query({
1205
+ TableName: tableName,
1206
+ IndexName: "GSI2",
1207
+ KeyConditionExpression: "GSI2PK = :pk",
1208
+ ExpressionAttributeValues: { ":pk": "ORDER#OPEN" },
1209
+ ExclusiveStartKey: { PK: "USER#1", SK: "ORDER#001" },
1210
+ })
1211
+ .promise(),
1212
+ },
1213
+ {
1214
+ id: "query.missing-expression-attribute-name",
1215
+ method: "query",
1216
+ setup: createSeed,
1217
+ execute: ({ client, tableName }) =>
1218
+ client.documentClient
1219
+ .query({
1220
+ TableName: tableName,
1221
+ KeyConditionExpression: "#pk = :pk",
1222
+ ExpressionAttributeValues: { ":pk": "USER#1" },
1223
+ })
1224
+ .promise(),
1225
+ },
1226
+ {
1227
+ id: "query.limit-non-integer",
1228
+ method: "query",
1229
+ setup: createSeed,
1230
+ execute: ({ client, tableName }) =>
1231
+ client.documentClient
1232
+ .query({
1233
+ TableName: tableName,
1234
+ KeyConditionExpression: "PK = :pk",
1235
+ ExpressionAttributeValues: { ":pk": "USER#1" },
1236
+ Limit: 1.5,
1237
+ })
1238
+ .promise(),
1239
+ },
1240
+ {
1241
+ id: "update.if-not-exists-existing-plus",
1242
+ method: "update",
1243
+ setup: createSeed,
1244
+ execute: ({ client, tableName }) =>
1245
+ client.documentClient
1246
+ .update({
1247
+ TableName: tableName,
1248
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
1249
+ UpdateExpression: "SET #score = if_not_exists(#score, :zero) + :inc",
1250
+ ExpressionAttributeNames: { "#score": "score" },
1251
+ ExpressionAttributeValues: { ":zero": 0, ":inc": 2 },
1252
+ ReturnValues: "ALL_NEW",
1253
+ })
1254
+ .promise(),
1255
+ },
1256
+ {
1257
+ id: "update.if-not-exists-missing-plus",
1258
+ method: "update",
1259
+ setup: createSeed,
1260
+ execute: ({ client, tableName }) =>
1261
+ client.documentClient
1262
+ .update({
1263
+ TableName: tableName,
1264
+ Key: { PK: "USER#2", SK: "PROFILE#001" },
1265
+ UpdateExpression: "SET #visits = if_not_exists(#visits, :zero) + :inc",
1266
+ ExpressionAttributeNames: { "#visits": "visits" },
1267
+ ExpressionAttributeValues: { ":zero": 0, ":inc": 1 },
1268
+ ReturnValues: "ALL_NEW",
1269
+ })
1270
+ .promise(),
1271
+ },
1272
+ {
1273
+ id: "update.list-append-with-if-not-exists",
1274
+ method: "update",
1275
+ setup: createSeed,
1276
+ execute: ({ client, tableName }) =>
1277
+ client.documentClient
1278
+ .update({
1279
+ TableName: tableName,
1280
+ Key: { PK: "USER#2", SK: "PROFILE#001" },
1281
+ UpdateExpression:
1282
+ "SET #tags = list_append(if_not_exists(#tags, :empty), :more)",
1283
+ ExpressionAttributeNames: { "#tags": "tags" },
1284
+ ExpressionAttributeValues: { ":empty": [], ":more": ["new", "vip"] },
1285
+ ReturnValues: "ALL_NEW",
1286
+ })
1287
+ .promise(),
1288
+ },
1289
+ {
1290
+ id: "scan.filter.contains",
1291
+ method: "scan",
1292
+ setup: createSeed,
1293
+ execute: ({ client, tableName }) =>
1294
+ client.documentClient
1295
+ .scan({
1296
+ TableName: tableName,
1297
+ FilterExpression: "contains(#tags, :tag)",
1298
+ ExpressionAttributeNames: { "#tags": "tags" },
1299
+ ExpressionAttributeValues: { ":tag": "math" },
1300
+ })
1301
+ .promise(),
1302
+ },
1303
+ {
1304
+ id: "scan.filter.attribute-type-and-size",
1305
+ method: "scan",
1306
+ setup: createSeed,
1307
+ execute: ({ client, tableName }) =>
1308
+ client.documentClient
1309
+ .scan({
1310
+ TableName: tableName,
1311
+ FilterExpression: "attribute_type(#name, :t) and size(#name) > :min",
1312
+ ExpressionAttributeNames: { "#name": "name" },
1313
+ ExpressionAttributeValues: { ":t": "S", ":min": 2 },
1314
+ })
1315
+ .promise(),
1316
+ },
1317
+ {
1318
+ id: "scan.missing-expression-attribute-value",
1319
+ method: "scan",
1320
+ setup: createSeed,
1321
+ execute: ({ client, tableName }) =>
1322
+ client.documentClient
1323
+ .scan({
1324
+ TableName: tableName,
1325
+ FilterExpression: "#status = :missing",
1326
+ ExpressionAttributeNames: { "#status": "status" },
1327
+ })
1328
+ .promise(),
1329
+ },
1330
+ {
1331
+ id: "batch-get.multi-table",
1332
+ method: "batchGet",
1333
+ setup: createSeed,
1334
+ execute: async (ctx) => {
1335
+ const auxTable = `${ctx.tableName}-aux-batch-get`
1336
+ await createAuxTableIfNeeded(ctx, auxTable)
1337
+
1338
+ try {
1339
+ await ctx.client.documentClient
1340
+ .batchWrite({
1341
+ RequestItems: {
1342
+ [auxTable]: [
1343
+ {
1344
+ PutRequest: {
1345
+ Item: { PK: "AUX#1", SK: "ITEM#1", flag: true },
1346
+ },
1347
+ },
1348
+ ],
1349
+ },
1350
+ })
1351
+ .promise()
1352
+
1353
+ return await ctx.client.documentClient
1354
+ .batchGet({
1355
+ RequestItems: {
1356
+ [ctx.tableName]: {
1357
+ Keys: [{ PK: "USER#1", SK: "PROFILE#001" }],
1358
+ },
1359
+ [auxTable]: {
1360
+ Keys: [{ PK: "AUX#1", SK: "ITEM#1" }],
1361
+ },
1362
+ },
1363
+ })
1364
+ .promise()
1365
+ } finally {
1366
+ await destroyAuxTableIfNeeded(ctx, auxTable)
1367
+ }
1368
+ },
1369
+ },
1370
+ {
1371
+ id: "batch-write.multi-table",
1372
+ method: "batchWrite",
1373
+ setup: createSeed,
1374
+ normalizeResult: () => ({ UnprocessedItems: {} }),
1375
+ execute: async (ctx) => {
1376
+ const auxTable = `${ctx.tableName}-aux-batch-write`
1377
+ await createAuxTableIfNeeded(ctx, auxTable)
1378
+
1379
+ try {
1380
+ return await ctx.client.documentClient
1381
+ .batchWrite({
1382
+ RequestItems: {
1383
+ [ctx.tableName]: [
1384
+ {
1385
+ PutRequest: { Item: { PK: "USER#70", SK: "PROFILE#001", ok: true } },
1386
+ },
1387
+ ],
1388
+ [auxTable]: [
1389
+ {
1390
+ PutRequest: { Item: { PK: "AUX#2", SK: "ITEM#1", ok: true } },
1391
+ },
1392
+ ],
1393
+ },
1394
+ })
1395
+ .promise()
1396
+ } finally {
1397
+ await destroyAuxTableIfNeeded(ctx, auxTable)
1398
+ }
1399
+ },
1400
+ },
1401
+ {
1402
+ id: "transact-write.rollback-on-parser-error-mid-transaction",
1403
+ method: "transactWrite",
1404
+ setup: createSeed,
1405
+ execute: ({ client, tableName }) =>
1406
+ client.documentClient
1407
+ .transactWrite({
1408
+ TransactItems: [
1409
+ {
1410
+ Update: {
1411
+ TableName: tableName,
1412
+ Key: { PK: "USER#2", SK: "PROFILE#001" },
1413
+ UpdateExpression: "SET #score = :score",
1414
+ ExpressionAttributeNames: { "#score": "score" },
1415
+ ExpressionAttributeValues: { ":score": 999 },
1416
+ },
1417
+ },
1418
+ {
1419
+ Update: {
1420
+ TableName: tableName,
1421
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
1422
+ UpdateExpression: "SET",
1423
+ },
1424
+ },
1425
+ ],
1426
+ })
1427
+ .promise(),
1428
+ },
1429
+ {
1430
+ id: "transact-write.key-mutation-error",
1431
+ method: "transactWrite",
1432
+ setup: createSeed,
1433
+ execute: ({ client, tableName }) =>
1434
+ client.documentClient
1435
+ .transactWrite({
1436
+ TransactItems: [
1437
+ {
1438
+ Update: {
1439
+ TableName: tableName,
1440
+ Key: { PK: "USER#2", SK: "PROFILE#001" },
1441
+ UpdateExpression: "SET PK = :pk",
1442
+ ExpressionAttributeValues: { ":pk": "USER#X" },
1443
+ },
1444
+ },
1445
+ ],
1446
+ })
1447
+ .promise(),
1448
+ },
1449
+ ]
1450
+
1451
+ const supportedParamCoverageVectors: Vector[] = [
1452
+ {
1453
+ id: "coverage.get.supported",
1454
+ method: "get",
1455
+ setup: createSeed,
1456
+ coverage: { supported: ["TableName", "Key", "ConsistentRead"] },
1457
+ execute: ({ client, tableName }) =>
1458
+ client.documentClient
1459
+ .get({
1460
+ TableName: tableName,
1461
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
1462
+ ConsistentRead: true,
1463
+ })
1464
+ .promise(),
1465
+ },
1466
+ {
1467
+ id: "coverage.put.supported",
1468
+ method: "put",
1469
+ setup: createSeed,
1470
+ coverage: {
1471
+ supported: [
1472
+ "TableName",
1473
+ "Item",
1474
+ "ConditionExpression",
1475
+ "ExpressionAttributeNames",
1476
+ "ExpressionAttributeValues",
1477
+ ],
1478
+ },
1479
+ execute: ({ client, tableName }) =>
1480
+ client.documentClient
1481
+ .put({
1482
+ TableName: tableName,
1483
+ Item: { PK: "COVER#PUT", SK: "1", status: "ok" },
1484
+ ConditionExpression: "attribute_not_exists(#pk) and :v = :v",
1485
+ ExpressionAttributeNames: { "#pk": "PK" },
1486
+ ExpressionAttributeValues: { ":v": "ok" },
1487
+ })
1488
+ .promise(),
1489
+ },
1490
+ {
1491
+ id: "coverage.update.supported",
1492
+ method: "update",
1493
+ setup: createSeed,
1494
+ coverage: {
1495
+ supported: [
1496
+ "TableName",
1497
+ "Key",
1498
+ "ConditionExpression",
1499
+ "UpdateExpression",
1500
+ "ExpressionAttributeNames",
1501
+ "ExpressionAttributeValues",
1502
+ "ReturnValues",
1503
+ ],
1504
+ },
1505
+ execute: ({ client, tableName }) =>
1506
+ client.documentClient
1507
+ .update({
1508
+ TableName: tableName,
1509
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
1510
+ ConditionExpression: "attribute_exists(PK)",
1511
+ UpdateExpression: "SET #score = :score",
1512
+ ExpressionAttributeNames: { "#score": "score" },
1513
+ ExpressionAttributeValues: { ":score": 12 },
1514
+ ReturnValues: "ALL_NEW",
1515
+ })
1516
+ .promise(),
1517
+ },
1518
+ {
1519
+ id: "coverage.delete.supported",
1520
+ method: "delete",
1521
+ setup: createSeed,
1522
+ coverage: {
1523
+ supported: [
1524
+ "TableName",
1525
+ "Key",
1526
+ "ConditionExpression",
1527
+ "ExpressionAttributeNames",
1528
+ "ExpressionAttributeValues",
1529
+ ],
1530
+ },
1531
+ execute: ({ client, tableName }) =>
1532
+ client.documentClient
1533
+ .delete({
1534
+ TableName: tableName,
1535
+ Key: { PK: "USER#3", SK: "PROFILE#001" },
1536
+ ConditionExpression: "#status = :status",
1537
+ ExpressionAttributeNames: { "#status": "status" },
1538
+ ExpressionAttributeValues: { ":status": "active" },
1539
+ })
1540
+ .promise(),
1541
+ },
1542
+ {
1543
+ id: "coverage.query.supported",
1544
+ method: "query",
1545
+ setup: createSeed,
1546
+ coverage: {
1547
+ supported: [
1548
+ "TableName",
1549
+ "IndexName",
1550
+ "KeyConditionExpression",
1551
+ "FilterExpression",
1552
+ "ExpressionAttributeNames",
1553
+ "ExpressionAttributeValues",
1554
+ "Limit",
1555
+ "ExclusiveStartKey",
1556
+ "ScanIndexForward",
1557
+ "ConsistentRead",
1558
+ ],
1559
+ },
1560
+ execute: async ({ client, tableName }) => {
1561
+ const firstPrimary = await client.documentClient
1562
+ .query({
1563
+ TableName: tableName,
1564
+ KeyConditionExpression: "PK = :pk",
1565
+ FilterExpression: "attribute_exists(#status)",
1566
+ ExpressionAttributeNames: { "#status": "status" },
1567
+ ExpressionAttributeValues: { ":pk": "USER#1" },
1568
+ ScanIndexForward: true,
1569
+ Limit: 1,
1570
+ })
1571
+ .promise()
1572
+
1573
+ const secondPrimary = await client.documentClient
1574
+ .query({
1575
+ TableName: tableName,
1576
+ KeyConditionExpression: "PK = :pk",
1577
+ FilterExpression: "attribute_exists(#status)",
1578
+ ExpressionAttributeNames: { "#status": "status" },
1579
+ ExpressionAttributeValues: { ":pk": "USER#1" },
1580
+ ScanIndexForward: true,
1581
+ Limit: 2,
1582
+ ExclusiveStartKey: firstPrimary.LastEvaluatedKey,
1583
+ })
1584
+ .promise()
1585
+
1586
+ const gsi = await client.documentClient
1587
+ .query({
1588
+ TableName: tableName,
1589
+ IndexName: "GSI2",
1590
+ KeyConditionExpression: "GSI2PK = :pk",
1591
+ ExpressionAttributeValues: { ":pk": "ORDER#OPEN" },
1592
+ ConsistentRead: false,
1593
+ Limit: 1,
1594
+ })
1595
+ .promise()
1596
+
1597
+ return { secondPrimary, gsi }
1598
+ },
1599
+ },
1600
+ {
1601
+ id: "coverage.scan.supported",
1602
+ method: "scan",
1603
+ setup: createSeed,
1604
+ coverage: {
1605
+ supported: [
1606
+ "TableName",
1607
+ "FilterExpression",
1608
+ "ExpressionAttributeNames",
1609
+ "ExpressionAttributeValues",
1610
+ "Limit",
1611
+ "ExclusiveStartKey",
1612
+ ],
1613
+ },
1614
+ execute: async ({ client, tableName }) => {
1615
+ const first = await client.documentClient
1616
+ .scan({
1617
+ TableName: tableName,
1618
+ FilterExpression: "#status = :status",
1619
+ ExpressionAttributeNames: { "#status": "status" },
1620
+ ExpressionAttributeValues: { ":status": "active" },
1621
+ Limit: 1,
1622
+ })
1623
+ .promise()
1624
+
1625
+ await client.documentClient
1626
+ .scan({
1627
+ TableName: tableName,
1628
+ FilterExpression: "#status = :status",
1629
+ ExpressionAttributeNames: { "#status": "status" },
1630
+ ExpressionAttributeValues: { ":status": "active" },
1631
+ Limit: 2,
1632
+ ExclusiveStartKey: first.LastEvaluatedKey,
1633
+ })
1634
+ .promise()
1635
+
1636
+ return client.documentClient
1637
+ .scan({
1638
+ TableName: tableName,
1639
+ FilterExpression: "#status = :status",
1640
+ ExpressionAttributeNames: { "#status": "status" },
1641
+ ExpressionAttributeValues: { ":status": "active" },
1642
+ })
1643
+ .promise()
1644
+ },
1645
+ },
1646
+ {
1647
+ id: "coverage.batchGet.supported",
1648
+ method: "batchGet",
1649
+ setup: createSeed,
1650
+ coverage: { supported: ["RequestItems"] },
1651
+ execute: ({ client, tableName }) =>
1652
+ client.documentClient
1653
+ .batchGet({
1654
+ RequestItems: { [tableName]: { Keys: [{ PK: "USER#1", SK: "PROFILE#001" }] } },
1655
+ })
1656
+ .promise(),
1657
+ },
1658
+ {
1659
+ id: "coverage.batchWrite.supported",
1660
+ method: "batchWrite",
1661
+ setup: createSeed,
1662
+ coverage: { supported: ["RequestItems"] },
1663
+ execute: ({ client, tableName }) =>
1664
+ client.documentClient
1665
+ .batchWrite({
1666
+ RequestItems: {
1667
+ [tableName]: [{ PutRequest: { Item: { PK: "COVER#BW", SK: "1" } } }],
1668
+ },
1669
+ })
1670
+ .promise(),
1671
+ },
1672
+ {
1673
+ id: "coverage.transactWrite.supported",
1674
+ method: "transactWrite",
1675
+ setup: createSeed,
1676
+ coverage: { supported: ["TransactItems"] },
1677
+ execute: ({ client, tableName }) =>
1678
+ client.documentClient
1679
+ .transactWrite({
1680
+ TransactItems: [
1681
+ {
1682
+ ConditionCheck: {
1683
+ TableName: tableName,
1684
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
1685
+ ConditionExpression: "attribute_exists(PK)",
1686
+ },
1687
+ },
1688
+ ],
1689
+ })
1690
+ .promise(),
1691
+ },
1692
+ ]
1693
+
1694
+ const unsupportedParamVectors: Vector[] = [
1695
+ ...Object.entries(IN_MEMORY_SPEC.methods).flatMap(([method, spec]) =>
1696
+ (spec.unsupportedParams ?? []).map((param) => ({
1697
+ id: `unsupported.${method}.${param}`,
1698
+ method: method as keyof typeof IN_MEMORY_SPEC.methods,
1699
+ coverage: { unsupported: [param] },
1700
+ setup:
1701
+ method === "get" ||
1702
+ method === "put" ||
1703
+ method === "update" ||
1704
+ method === "delete" ||
1705
+ method === "query" ||
1706
+ method === "scan" ||
1707
+ method === "batchGet" ||
1708
+ method === "batchWrite" ||
1709
+ method === "transactWrite"
1710
+ ? createSeed
1711
+ : undefined,
1712
+ execute: (ctx: Context) => {
1713
+ const baseByMethod: Record<string, any> = {
1714
+ get: {
1715
+ TableName: ctx.tableName,
1716
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
1717
+ },
1718
+ put: {
1719
+ TableName: ctx.tableName,
1720
+ Item: { PK: "UNSUPPORTED#PUT", SK: "1" },
1721
+ },
1722
+ update: {
1723
+ TableName: ctx.tableName,
1724
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
1725
+ UpdateExpression: "SET #name = :name",
1726
+ ExpressionAttributeNames: { "#name": "name" },
1727
+ ExpressionAttributeValues: { ":name": "x" },
1728
+ },
1729
+ delete: {
1730
+ TableName: ctx.tableName,
1731
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
1732
+ },
1733
+ query: {
1734
+ TableName: ctx.tableName,
1735
+ KeyConditionExpression: "PK = :pk",
1736
+ ExpressionAttributeValues: { ":pk": "USER#1" },
1737
+ },
1738
+ scan: {
1739
+ TableName: ctx.tableName,
1740
+ },
1741
+ batchGet: {
1742
+ RequestItems: {
1743
+ [ctx.tableName]: {
1744
+ Keys: [{ PK: "USER#1", SK: "PROFILE#001" }],
1745
+ },
1746
+ },
1747
+ },
1748
+ batchWrite: {
1749
+ RequestItems: {
1750
+ [ctx.tableName]: [{ PutRequest: { Item: { PK: "UNSUPPORTED#BW", SK: "1" } } }],
1751
+ },
1752
+ },
1753
+ transactWrite: {
1754
+ TransactItems: [
1755
+ {
1756
+ ConditionCheck: {
1757
+ TableName: ctx.tableName,
1758
+ Key: { PK: "USER#1", SK: "PROFILE#001" },
1759
+ ConditionExpression: "attribute_exists(PK)",
1760
+ },
1761
+ },
1762
+ ],
1763
+ },
1764
+ }
1765
+
1766
+ const valueByParam: Record<string, any> = {
1767
+ AttributesToGet: ["PK"],
1768
+ ProjectionExpression: "PK",
1769
+ ExpressionAttributeNames: { "#pk": "PK" },
1770
+ Expected: {},
1771
+ ReturnValues: "ALL_OLD",
1772
+ ReturnConsumedCapacity: "TOTAL",
1773
+ ReturnItemCollectionMetrics: "SIZE",
1774
+ AttributeUpdates: {},
1775
+ Select: "ALL_ATTRIBUTES",
1776
+ KeyConditions: {},
1777
+ QueryFilter: {},
1778
+ ConditionalOperator: "AND",
1779
+ Segment: 1,
1780
+ TotalSegments: 2,
1781
+ ScanFilter: {},
1782
+ ClientRequestToken: "token-1",
1783
+ }
1784
+
1785
+ const params = {
1786
+ ...baseByMethod[method],
1787
+ [param]: valueByParam[param],
1788
+ }
1789
+
1790
+ return (ctx.client.documentClient as any)[method](params).promise()
1791
+ },
1792
+ }))
1793
+ ),
1794
+ ]
1795
+
1796
+ const seededFuzzVectors: Vector[] = [11, 42, 99].map((seed) => ({
1797
+ id: `fuzz.seed-${seed}`,
1798
+ method: "transactWrite",
1799
+ execute: async ({ client, tableName }) => {
1800
+ const state = {
1801
+ seed,
1802
+ next: seed,
1803
+ }
1804
+
1805
+ const random = () => {
1806
+ state.next = (state.next * 48271) % 0x7fffffff
1807
+ return state.next / 0x7fffffff
1808
+ }
1809
+
1810
+ const results: any[] = []
1811
+
1812
+ for (let i = 0; i < 80; i += 1) {
1813
+ const dice = random()
1814
+ const user = `F#${Math.floor(random() * 8)}`
1815
+ const key = { PK: user, SK: `S#${Math.floor(random() * 5)}` }
1816
+
1817
+ if (dice < 0.25) {
1818
+ try {
1819
+ await client.documentClient
1820
+ .put({
1821
+ TableName: tableName,
1822
+ Item: {
1823
+ ...key,
1824
+ GSI2PK: `GX#${key.PK}`,
1825
+ GSI2SK: key.SK,
1826
+ score: Math.floor(random() * 100),
1827
+ status: random() > 0.5 ? "active" : "pending",
1828
+ },
1829
+ })
1830
+ .promise()
1831
+ results.push({ op: "put", key, ok: true })
1832
+ } catch (error) {
1833
+ results.push({ op: "put", key, ok: false, error: normalizeError(error) })
1834
+ }
1835
+ continue
1836
+ }
1837
+
1838
+ if (dice < 0.5) {
1839
+ try {
1840
+ const mutateGsi = random() > 0.6
1841
+ const response = await client.documentClient
1842
+ .update({
1843
+ TableName: tableName,
1844
+ Key: key,
1845
+ UpdateExpression: mutateGsi
1846
+ ? random() > 0.5
1847
+ ? "SET #score = :score, GSI2PK = :gpk, GSI2SK = :gsk"
1848
+ : "SET #score = :score REMOVE GSI2PK, GSI2SK"
1849
+ : "SET #score = :score",
1850
+ ExpressionAttributeNames: { "#score": "score" },
1851
+ ExpressionAttributeValues: mutateGsi
1852
+ ? {
1853
+ ":score": Math.floor(random() * 100),
1854
+ ":gpk": `GX#${key.PK}`,
1855
+ ":gsk": key.SK,
1856
+ }
1857
+ : { ":score": Math.floor(random() * 100) },
1858
+ ReturnValues: "ALL_NEW",
1859
+ })
1860
+ .promise()
1861
+
1862
+ results.push({
1863
+ op: "update",
1864
+ key,
1865
+ ok: true,
1866
+ attrs: normalizeObject(response.Attributes ?? {}),
1867
+ })
1868
+ } catch (error) {
1869
+ results.push({ op: "update", key, ok: false, error: normalizeError(error) })
1870
+ }
1871
+ continue
1872
+ }
1873
+
1874
+ if (dice < 0.7) {
1875
+ try {
1876
+ await client.documentClient
1877
+ .delete({
1878
+ TableName: tableName,
1879
+ Key: key,
1880
+ ConditionExpression: "attribute_not_exists(blocked)",
1881
+ })
1882
+ .promise()
1883
+ results.push({ op: "delete", key, ok: true })
1884
+ } catch (error) {
1885
+ results.push({ op: "delete", key, ok: false, error: normalizeError(error) })
1886
+ }
1887
+ continue
1888
+ }
1889
+
1890
+ if (dice < 0.85) {
1891
+ try {
1892
+ const useGsi = random() > 0.5
1893
+ const response = useGsi
1894
+ ? await client.documentClient
1895
+ .query({
1896
+ TableName: tableName,
1897
+ IndexName: "GSI2",
1898
+ KeyConditionExpression: "GSI2PK = :pk",
1899
+ ExpressionAttributeValues: { ":pk": `GX#${user}` },
1900
+ Limit: 3,
1901
+ })
1902
+ .promise()
1903
+ : await client.documentClient
1904
+ .query({
1905
+ TableName: tableName,
1906
+ KeyConditionExpression: "PK = :pk",
1907
+ ExpressionAttributeValues: { ":pk": user },
1908
+ Limit: 3,
1909
+ })
1910
+ .promise()
1911
+
1912
+ results.push({
1913
+ op: useGsi ? "query-gsi" : "query",
1914
+ key,
1915
+ ok: true,
1916
+ count: response.Count,
1917
+ scannedCount: response.ScannedCount,
1918
+ items: normalizeObject(response.Items ?? []),
1919
+ })
1920
+ } catch (error) {
1921
+ results.push({ op: "query", key, ok: false, error: normalizeError(error) })
1922
+ }
1923
+ continue
1924
+ }
1925
+
1926
+ try {
1927
+ await client.documentClient
1928
+ .transactWrite({
1929
+ TransactItems: [
1930
+ {
1931
+ Put: {
1932
+ TableName: tableName,
1933
+ Item: {
1934
+ PK: `${user}#tx`,
1935
+ SK: key.SK,
1936
+ score: Math.floor(random() * 100),
1937
+ },
1938
+ },
1939
+ },
1940
+ {
1941
+ ConditionCheck: {
1942
+ TableName: tableName,
1943
+ Key: key,
1944
+ ConditionExpression:
1945
+ random() > 0.5 ? "attribute_exists(PK)" : "attribute_not_exists(PK)",
1946
+ },
1947
+ },
1948
+ ],
1949
+ })
1950
+ .promise()
1951
+
1952
+ results.push({ op: "transactWrite", key, ok: true })
1953
+ } catch (error) {
1954
+ results.push({
1955
+ op: "transactWrite",
1956
+ key,
1957
+ ok: false,
1958
+ error: normalizeError(error),
1959
+ })
1960
+ }
1961
+ }
1962
+
1963
+ return results
1964
+ },
1965
+ }))
1966
+
1967
+ describe("dynamodb conformance (local vs in-memory)", () => {
1968
+ const differentialVectors = [
1969
+ ...generatedVectors,
1970
+ ...additionalDifferentialVectors,
1971
+ ...supportedParamCoverageVectors,
1972
+ ...seededFuzzVectors,
1973
+ ]
1974
+
1975
+ test("every supported method in the manifest has differential vector coverage", () => {
1976
+ const coveredMethods = new Set(differentialVectors.map((vector) => vector.method))
1977
+ const expectedMethods = new Set(Object.keys(IN_MEMORY_SPEC.methods))
1978
+
1979
+ expect(coveredMethods).toEqual(expectedMethods)
1980
+ })
1981
+
1982
+ test("supported and unsupported parameter coverage matches the spec", () => {
1983
+ const supportedCoverage = new Map<string, Set<string>>()
1984
+ const unsupportedCoverage = new Map<string, Set<string>>()
1985
+
1986
+ for (const vector of [...differentialVectors, ...unsupportedParamVectors]) {
1987
+ if (!vector.coverage?.supported && !vector.coverage?.unsupported) continue
1988
+
1989
+ const method = vector.method as string
1990
+
1991
+ if (!supportedCoverage.has(method)) supportedCoverage.set(method, new Set())
1992
+ if (!unsupportedCoverage.has(method)) unsupportedCoverage.set(method, new Set())
1993
+
1994
+ for (const param of vector.coverage?.supported ?? []) {
1995
+ supportedCoverage.get(method)!.add(param)
1996
+ }
1997
+
1998
+ for (const param of vector.coverage?.unsupported ?? []) {
1999
+ unsupportedCoverage.get(method)!.add(param)
2000
+ }
2001
+ }
2002
+
2003
+ for (const [method, spec] of Object.entries(IN_MEMORY_SPEC.methods)) {
2004
+ expect(supportedCoverage.get(method) ?? new Set()).toEqual(
2005
+ new Set(spec.supportedParams)
2006
+ )
2007
+ expect(unsupportedCoverage.get(method) ?? new Set()).toEqual(
2008
+ new Set(spec.unsupportedParams ?? [])
2009
+ )
2010
+ }
2011
+ })
2012
+
2013
+ test.each(differentialVectors)("vector $id", async (vector) => {
2014
+ const [local, memory] = await Promise.all([
2015
+ runVector("local", vector),
2016
+ runVector("memory", vector),
2017
+ ])
2018
+
2019
+ expect(memory).toEqual(local)
2020
+ })
2021
+
2022
+ test.each(unsupportedParamVectors)(
2023
+ "unsupported vector $id throws deterministic NotSupportedError in memory",
2024
+ async (vector) => {
2025
+ const result = await runVector("memory", vector)
2026
+ expect(result.ok).toBe(false)
2027
+ if (result.ok) return
2028
+
2029
+ const [methodName, param] = vector.id
2030
+ .replace("unsupported.", "")
2031
+ .split(".")
2032
+
2033
+ expect(result.error).toEqual({
2034
+ code: "NotSupportedError",
2035
+ message: `${param} is not supported in in-memory mode`,
2036
+ method: methodName,
2037
+ featurePath: `${methodName}.${param}`,
2038
+ reason: `${param} is not supported in in-memory mode.`,
2039
+ })
2040
+ }
2041
+ )
2042
+ })