@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,239 @@
|
|
|
1
|
+
interface TreapNode<V> {
|
|
2
|
+
key: string
|
|
3
|
+
priority: number
|
|
4
|
+
value: V
|
|
5
|
+
left: TreapNode<V> | null
|
|
6
|
+
right: TreapNode<V> | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TreapBounds {
|
|
10
|
+
lower?: { key: string; inclusive: boolean }
|
|
11
|
+
upper?: { key: string; inclusive: boolean }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class DeterministicTreap<V> {
|
|
15
|
+
private root: TreapNode<V> | null = null
|
|
16
|
+
private _size = 0
|
|
17
|
+
|
|
18
|
+
get size() {
|
|
19
|
+
return this._size
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
insert(key: string, value: V, priority: number): void {
|
|
23
|
+
const [nextRoot, inserted] = this.insertNode(this.root, key, value, priority)
|
|
24
|
+
this.root = nextRoot
|
|
25
|
+
if (inserted) this._size += 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
remove(key: string): boolean {
|
|
29
|
+
const [nextRoot, removed] = this.removeNode(this.root, key)
|
|
30
|
+
this.root = nextRoot
|
|
31
|
+
if (removed) this._size -= 1
|
|
32
|
+
return removed
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
has(key: string): boolean {
|
|
36
|
+
let current = this.root
|
|
37
|
+
while (current) {
|
|
38
|
+
if (key === current.key) return true
|
|
39
|
+
current = key < current.key ? current.left : current.right
|
|
40
|
+
}
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
*iterate(
|
|
45
|
+
direction: "asc" | "desc",
|
|
46
|
+
bounds: TreapBounds = {}
|
|
47
|
+
): IterableIterator<{ key: string; value: V }> {
|
|
48
|
+
if (direction === "asc") {
|
|
49
|
+
yield* this.iterateAsc(bounds)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
yield* this.iterateDesc(bounds)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
clear() {
|
|
57
|
+
this.root = null
|
|
58
|
+
this._size = 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private rotateRight(node: TreapNode<V>): TreapNode<V> {
|
|
62
|
+
const left = node.left
|
|
63
|
+
if (!left) return node
|
|
64
|
+
|
|
65
|
+
node.left = left.right
|
|
66
|
+
left.right = node
|
|
67
|
+
return left
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private rotateLeft(node: TreapNode<V>): TreapNode<V> {
|
|
71
|
+
const right = node.right
|
|
72
|
+
if (!right) return node
|
|
73
|
+
|
|
74
|
+
node.right = right.left
|
|
75
|
+
right.left = node
|
|
76
|
+
return right
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private insertNode(
|
|
80
|
+
node: TreapNode<V> | null,
|
|
81
|
+
key: string,
|
|
82
|
+
value: V,
|
|
83
|
+
priority: number
|
|
84
|
+
): [TreapNode<V>, boolean] {
|
|
85
|
+
if (!node) {
|
|
86
|
+
return [
|
|
87
|
+
{
|
|
88
|
+
key,
|
|
89
|
+
priority,
|
|
90
|
+
value,
|
|
91
|
+
left: null,
|
|
92
|
+
right: null,
|
|
93
|
+
},
|
|
94
|
+
true,
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (key === node.key) {
|
|
99
|
+
node.value = value
|
|
100
|
+
return [node, false]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (key < node.key) {
|
|
104
|
+
const [next, inserted] = this.insertNode(node.left, key, value, priority)
|
|
105
|
+
node.left = next
|
|
106
|
+
|
|
107
|
+
if (node.left && node.left.priority < node.priority) {
|
|
108
|
+
node = this.rotateRight(node)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return [node, inserted]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const [next, inserted] = this.insertNode(node.right, key, value, priority)
|
|
115
|
+
node.right = next
|
|
116
|
+
|
|
117
|
+
if (node.right && node.right.priority < node.priority) {
|
|
118
|
+
node = this.rotateLeft(node)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return [node, inserted]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private removeNode(
|
|
125
|
+
node: TreapNode<V> | null,
|
|
126
|
+
key: string
|
|
127
|
+
): [TreapNode<V> | null, boolean] {
|
|
128
|
+
if (!node) return [null, false]
|
|
129
|
+
|
|
130
|
+
if (key < node.key) {
|
|
131
|
+
const [next, removed] = this.removeNode(node.left, key)
|
|
132
|
+
node.left = next
|
|
133
|
+
return [node, removed]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (key > node.key) {
|
|
137
|
+
const [next, removed] = this.removeNode(node.right, key)
|
|
138
|
+
node.right = next
|
|
139
|
+
return [node, removed]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!node.left) return [node.right, true]
|
|
143
|
+
if (!node.right) return [node.left, true]
|
|
144
|
+
|
|
145
|
+
if (node.left.priority < node.right.priority) {
|
|
146
|
+
const rotated = this.rotateRight(node)
|
|
147
|
+
const [next, removed] = this.removeNode(rotated.right, key)
|
|
148
|
+
rotated.right = next
|
|
149
|
+
return [rotated, removed]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const rotated = this.rotateLeft(node)
|
|
153
|
+
const [next, removed] = this.removeNode(rotated.left, key)
|
|
154
|
+
rotated.left = next
|
|
155
|
+
return [rotated, removed]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private *iterateAsc(bounds: TreapBounds): IterableIterator<{ key: string; value: V }> {
|
|
159
|
+
const stack: TreapNode<V>[] = []
|
|
160
|
+
|
|
161
|
+
const pushLeft = (node: TreapNode<V> | null) => {
|
|
162
|
+
while (node) {
|
|
163
|
+
if (this.isBelowLowerBound(node.key, bounds.lower)) {
|
|
164
|
+
node = node.right
|
|
165
|
+
} else {
|
|
166
|
+
stack.push(node)
|
|
167
|
+
node = node.left
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
pushLeft(this.root)
|
|
173
|
+
|
|
174
|
+
while (stack.length) {
|
|
175
|
+
const node = stack.pop()!
|
|
176
|
+
|
|
177
|
+
if (this.isAboveUpperBound(node.key, bounds.upper)) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!this.isBelowLowerBound(node.key, bounds.lower)) {
|
|
182
|
+
yield { key: node.key, value: node.value }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
pushLeft(node.right)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private *iterateDesc(bounds: TreapBounds): IterableIterator<{ key: string; value: V }> {
|
|
190
|
+
const stack: TreapNode<V>[] = []
|
|
191
|
+
|
|
192
|
+
const pushRight = (node: TreapNode<V> | null) => {
|
|
193
|
+
while (node) {
|
|
194
|
+
if (this.isAboveUpperBound(node.key, bounds.upper)) {
|
|
195
|
+
node = node.left
|
|
196
|
+
} else {
|
|
197
|
+
stack.push(node)
|
|
198
|
+
node = node.right
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
pushRight(this.root)
|
|
204
|
+
|
|
205
|
+
while (stack.length) {
|
|
206
|
+
const node = stack.pop()!
|
|
207
|
+
|
|
208
|
+
if (this.isBelowLowerBound(node.key, bounds.lower)) {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!this.isAboveUpperBound(node.key, bounds.upper)) {
|
|
213
|
+
yield { key: node.key, value: node.value }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
pushRight(node.left)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private isBelowLowerBound(
|
|
221
|
+
key: string,
|
|
222
|
+
lower?: { key: string; inclusive: boolean }
|
|
223
|
+
): boolean {
|
|
224
|
+
if (!lower) return false
|
|
225
|
+
|
|
226
|
+
if (lower.inclusive) return key < lower.key
|
|
227
|
+
return key <= lower.key
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private isAboveUpperBound(
|
|
231
|
+
key: string,
|
|
232
|
+
upper?: { key: string; inclusive: boolean }
|
|
233
|
+
): boolean {
|
|
234
|
+
if (!upper) return false
|
|
235
|
+
|
|
236
|
+
if (upper.inclusive) return key > upper.key
|
|
237
|
+
return key >= upper.key
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import crypto from "crypto"
|
|
2
|
+
|
|
3
|
+
export type InMemoryItem = { [key: string]: any }
|
|
4
|
+
|
|
5
|
+
export const ITEM_KEY_SEPARATOR = "\u0000"
|
|
6
|
+
|
|
7
|
+
export const encodeCompositeKey = (...parts: string[]): string =>
|
|
8
|
+
parts
|
|
9
|
+
.map((part) => `${part.length}:${part}`)
|
|
10
|
+
.join(ITEM_KEY_SEPARATOR)
|
|
11
|
+
|
|
12
|
+
export const encodeItemKey = (pk: string, sk: string): string =>
|
|
13
|
+
encodeCompositeKey(pk, sk)
|
|
14
|
+
|
|
15
|
+
export const encodeIndexEntryKey = (rangeKey: string, itemKey: string): string =>
|
|
16
|
+
`${rangeKey}${ITEM_KEY_SEPARATOR}${itemKey}`
|
|
17
|
+
|
|
18
|
+
export const cloneItem = <T>(item: T): T => JSON.parse(JSON.stringify(item))
|
|
19
|
+
|
|
20
|
+
export const stablePriority = (
|
|
21
|
+
indexName: string,
|
|
22
|
+
hashKey: string,
|
|
23
|
+
rangeKey: string,
|
|
24
|
+
itemKey: string
|
|
25
|
+
): number => {
|
|
26
|
+
const hash = crypto
|
|
27
|
+
.createHash("sha256")
|
|
28
|
+
.update(`${indexName}::${hashKey}::${rangeKey}::${itemKey}`)
|
|
29
|
+
.digest()
|
|
30
|
+
|
|
31
|
+
return hash.readUInt32BE(0)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const sortItemsByPKSK = (items: InMemoryItem[]): InMemoryItem[] =>
|
|
35
|
+
[...items].sort((a, b) => {
|
|
36
|
+
const pkA = String(a.PK ?? "")
|
|
37
|
+
const pkB = String(b.PK ?? "")
|
|
38
|
+
if (pkA !== pkB) return pkA < pkB ? -1 : 1
|
|
39
|
+
|
|
40
|
+
const skA = String(a.SK ?? "")
|
|
41
|
+
const skB = String(b.SK ?? "")
|
|
42
|
+
if (skA !== skB) return skA < skB ? -1 : 1
|
|
43
|
+
|
|
44
|
+
return 0
|
|
45
|
+
})
|
package/src/sandbox.ts
CHANGED
|
@@ -4,6 +4,7 @@ import DynamoDB from "aws-sdk/clients/dynamodb"
|
|
|
4
4
|
import { formatSnapshotDiff } from "./diff"
|
|
5
5
|
import { Client } from "./client"
|
|
6
6
|
import { GSI_NAMES } from "./gsi"
|
|
7
|
+
import { createInMemoryDocumentClient } from "./in-memory"
|
|
7
8
|
|
|
8
9
|
const ddb = new DynamoDB({
|
|
9
10
|
accessKeyId: "xxx",
|
|
@@ -105,18 +106,241 @@ export const getTableContents = async (
|
|
|
105
106
|
return acc
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
// -------------------------------------------------------------------------------------
|
|
110
|
+
// Tracking
|
|
111
|
+
// -------------------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
interface TrackedEntry {
|
|
114
|
+
pk: string
|
|
115
|
+
sk: string
|
|
116
|
+
original: any | null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const WRITE_METHODS = new Set([
|
|
120
|
+
"put",
|
|
121
|
+
"update",
|
|
122
|
+
"delete",
|
|
123
|
+
"batchWrite",
|
|
124
|
+
"transactWrite",
|
|
125
|
+
])
|
|
126
|
+
|
|
127
|
+
function createTrackedDocClient(
|
|
128
|
+
original: DynamoDB.DocumentClient,
|
|
129
|
+
tableName: string
|
|
130
|
+
) {
|
|
131
|
+
let isTracking = false
|
|
132
|
+
const trackedKeys = new Map<string, TrackedEntry>()
|
|
133
|
+
|
|
134
|
+
const captureKey = async (pk: string, sk: string) => {
|
|
135
|
+
const compositeKey = `${pk}__${sk}`
|
|
136
|
+
if (trackedKeys.has(compositeKey)) return
|
|
137
|
+
|
|
138
|
+
const { Item } = await original
|
|
139
|
+
.get({ TableName: tableName, Key: { PK: pk, SK: sk } })
|
|
140
|
+
.promise()
|
|
141
|
+
|
|
142
|
+
trackedKeys.set(compositeKey, { pk, sk, original: Item ?? null })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const captureKeysForOperation = async (method: string, params: any) => {
|
|
146
|
+
switch (method) {
|
|
147
|
+
case "put":
|
|
148
|
+
if (params.TableName === tableName && params.Item) {
|
|
149
|
+
await captureKey(params.Item.PK, params.Item.SK)
|
|
150
|
+
}
|
|
151
|
+
break
|
|
152
|
+
case "update":
|
|
153
|
+
case "delete":
|
|
154
|
+
if (params.TableName === tableName && params.Key) {
|
|
155
|
+
await captureKey(params.Key.PK, params.Key.SK)
|
|
156
|
+
}
|
|
157
|
+
break
|
|
158
|
+
case "batchWrite": {
|
|
159
|
+
const tableItems = params.RequestItems?.[tableName] || []
|
|
160
|
+
await Promise.all(
|
|
161
|
+
tableItems.map((item: any) => {
|
|
162
|
+
if (item.PutRequest) {
|
|
163
|
+
return captureKey(
|
|
164
|
+
item.PutRequest.Item.PK,
|
|
165
|
+
item.PutRequest.Item.SK
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
if (item.DeleteRequest) {
|
|
169
|
+
return captureKey(
|
|
170
|
+
item.DeleteRequest.Key.PK,
|
|
171
|
+
item.DeleteRequest.Key.SK
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
break
|
|
177
|
+
}
|
|
178
|
+
case "transactWrite": {
|
|
179
|
+
const transactItems = params.TransactItems || []
|
|
180
|
+
await Promise.all(
|
|
181
|
+
transactItems
|
|
182
|
+
.map((item: any) => {
|
|
183
|
+
if (item.Put?.TableName === tableName) {
|
|
184
|
+
return captureKey(item.Put.Item.PK, item.Put.Item.SK)
|
|
185
|
+
}
|
|
186
|
+
if (item.Update?.TableName === tableName) {
|
|
187
|
+
return captureKey(item.Update.Key.PK, item.Update.Key.SK)
|
|
188
|
+
}
|
|
189
|
+
if (item.Delete?.TableName === tableName) {
|
|
190
|
+
return captureKey(item.Delete.Key.PK, item.Delete.Key.SK)
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
.filter(Boolean)
|
|
194
|
+
)
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const proxy = new Proxy(original, {
|
|
201
|
+
get(target, prop) {
|
|
202
|
+
const value = (target as any)[prop]
|
|
203
|
+
if (value === undefined) return undefined
|
|
204
|
+
|
|
205
|
+
if (typeof value === "function") {
|
|
206
|
+
if (isTracking && WRITE_METHODS.has(prop as string)) {
|
|
207
|
+
return (params: any) => {
|
|
208
|
+
const request = value.call(target, params)
|
|
209
|
+
const origPromise = request.promise.bind(request)
|
|
210
|
+
request.promise = async () => {
|
|
211
|
+
await captureKeysForOperation(prop as string, params)
|
|
212
|
+
return origPromise()
|
|
213
|
+
}
|
|
214
|
+
return request
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return value.bind(target)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return value
|
|
221
|
+
},
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
proxy: proxy as DynamoDB.DocumentClient,
|
|
226
|
+
startTracking: () => {
|
|
227
|
+
isTracking = true
|
|
228
|
+
trackedKeys.clear()
|
|
229
|
+
},
|
|
230
|
+
rollback: async () => {
|
|
231
|
+
isTracking = false
|
|
232
|
+
|
|
233
|
+
const entries = Array.from(trackedKeys.values())
|
|
234
|
+
const toDelete = entries.filter((e) => e.original === null)
|
|
235
|
+
const toRestore = entries.filter((e) => e.original !== null)
|
|
236
|
+
|
|
237
|
+
const deleteChunks = chunksOf(25)(toDelete)
|
|
238
|
+
const restoreChunks = chunksOf(25)(toRestore)
|
|
239
|
+
|
|
240
|
+
await Promise.all([
|
|
241
|
+
...deleteChunks.map((chunk) =>
|
|
242
|
+
original
|
|
243
|
+
.batchWrite({
|
|
244
|
+
RequestItems: {
|
|
245
|
+
[tableName]: chunk.map(({ pk, sk }) => ({
|
|
246
|
+
DeleteRequest: { Key: { PK: pk, SK: sk } },
|
|
247
|
+
})),
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
.promise()
|
|
251
|
+
),
|
|
252
|
+
...restoreChunks.map((chunk) =>
|
|
253
|
+
original
|
|
254
|
+
.batchWrite({
|
|
255
|
+
RequestItems: {
|
|
256
|
+
[tableName]: chunk.map(({ original: item }) => ({
|
|
257
|
+
PutRequest: { Item: item },
|
|
258
|
+
})),
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
.promise()
|
|
262
|
+
),
|
|
263
|
+
])
|
|
264
|
+
|
|
265
|
+
trackedKeys.clear()
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// -------------------------------------------------------------------------------------
|
|
271
|
+
// Sandbox
|
|
272
|
+
// -------------------------------------------------------------------------------------
|
|
273
|
+
|
|
108
274
|
export interface Sandbox {
|
|
109
275
|
destroy: () => Promise<void>
|
|
110
276
|
snapshot: () => Promise<{ [key: string]: any }>
|
|
111
277
|
seed: (...args: Array<{ [key: string]: any }>) => Promise<void>
|
|
112
278
|
get: (pk: string, sk: string) => Promise<null | any>
|
|
113
279
|
diff: (before: { [key: string]: any }) => Promise<string>
|
|
280
|
+
startTracking: () => void
|
|
281
|
+
rollback: () => Promise<void>
|
|
114
282
|
}
|
|
115
283
|
|
|
116
284
|
export const createSandbox = async (client: Client): Promise<Sandbox> => {
|
|
285
|
+
if (process.env.EXPERIMENTAL_DYNAMODB_IN_MEMORY === "1") {
|
|
286
|
+
const tableName = crypto.randomBytes(20).toString("hex")
|
|
287
|
+
const inMemoryClient =
|
|
288
|
+
createInMemoryDocumentClient() as any as DynamoDB.DocumentClient & {
|
|
289
|
+
__inMemorySnapshot: (name: string) => { [key: string]: any }
|
|
290
|
+
__inMemoryResetTable: (name: string) => void
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const tracked = createTrackedDocClient(inMemoryClient, tableName)
|
|
294
|
+
|
|
295
|
+
client.setDocumentClient(tracked.proxy)
|
|
296
|
+
client.setTableName(tableName)
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
destroy: async () => {
|
|
300
|
+
inMemoryClient.__inMemoryResetTable(tableName)
|
|
301
|
+
},
|
|
302
|
+
snapshot: async () => inMemoryClient.__inMemorySnapshot(tableName),
|
|
303
|
+
seed: async (...args: Array<{ [key: string]: any }>) => {
|
|
304
|
+
const chunks = chunksOf(25)(args)
|
|
305
|
+
|
|
306
|
+
await Promise.all(
|
|
307
|
+
chunks.map(async (chunk) => {
|
|
308
|
+
const items = chunk.map((i) =>
|
|
309
|
+
typeof i?._model?.__dynamoDBEncode === "function"
|
|
310
|
+
? i._model.__dynamoDBEncode(i)
|
|
311
|
+
: typeof i.encode === "function"
|
|
312
|
+
? i.encode()
|
|
313
|
+
: i
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
return client.documentClient
|
|
317
|
+
.batchWrite({
|
|
318
|
+
RequestItems: {
|
|
319
|
+
[tableName]: items.map((i) => ({ PutRequest: { Item: i } })),
|
|
320
|
+
},
|
|
321
|
+
})
|
|
322
|
+
.promise()
|
|
323
|
+
})
|
|
324
|
+
)
|
|
325
|
+
},
|
|
326
|
+
get: (pk: string, sk: string) =>
|
|
327
|
+
client.documentClient
|
|
328
|
+
.get({ TableName: tableName, Key: { PK: pk, SK: sk } })
|
|
329
|
+
.promise()
|
|
330
|
+
.then(({ Item }) => Item ?? null),
|
|
331
|
+
diff: async (before) => {
|
|
332
|
+
const snapshot = inMemoryClient.__inMemorySnapshot(tableName)
|
|
333
|
+
return formatSnapshotDiff(before, snapshot)
|
|
334
|
+
},
|
|
335
|
+
startTracking: tracked.startTracking,
|
|
336
|
+
rollback: tracked.rollback,
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
117
340
|
const tableName = await createTable()
|
|
118
341
|
|
|
119
|
-
|
|
342
|
+
const tracked = createTrackedDocClient(docClient, tableName)
|
|
343
|
+
client.setDocumentClient(tracked.proxy)
|
|
120
344
|
client.setTableName(tableName)
|
|
121
345
|
|
|
122
346
|
return {
|
|
@@ -155,5 +379,7 @@ export const createSandbox = async (client: Client): Promise<Sandbox> => {
|
|
|
155
379
|
|
|
156
380
|
return formatSnapshotDiff(before, snapshot)
|
|
157
381
|
},
|
|
382
|
+
startTracking: tracked.startTracking,
|
|
383
|
+
rollback: tracked.rollback,
|
|
158
384
|
}
|
|
159
385
|
}
|