@model-ts/dynamodb 4.1.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/__test__/client-env-guard.test.d.ts +1 -0
  3. package/dist/cjs/__test__/client-env-guard.test.js +28 -0
  4. package/dist/cjs/__test__/client-env-guard.test.js.map +1 -0
  5. package/dist/cjs/__test__/conformance.test.d.ts +1 -0
  6. package/dist/cjs/__test__/conformance.test.js +1835 -0
  7. package/dist/cjs/__test__/conformance.test.js.map +1 -0
  8. package/dist/cjs/__test__/in-memory.spec.test.d.ts +1 -0
  9. package/dist/cjs/__test__/in-memory.spec.test.js +234 -0
  10. package/dist/cjs/__test__/in-memory.spec.test.js.map +1 -0
  11. package/dist/cjs/client.d.ts +2 -2
  12. package/dist/cjs/client.js +14 -6
  13. package/dist/cjs/client.js.map +1 -1
  14. package/dist/cjs/errors.d.ts +13 -1
  15. package/dist/cjs/errors.js +12 -1
  16. package/dist/cjs/errors.js.map +1 -1
  17. package/dist/cjs/in-memory/document-client.d.ts +45 -0
  18. package/dist/cjs/in-memory/document-client.js +795 -0
  19. package/dist/cjs/in-memory/document-client.js.map +1 -0
  20. package/dist/cjs/in-memory/expression.d.ts +42 -0
  21. package/dist/cjs/in-memory/expression.js +665 -0
  22. package/dist/cjs/in-memory/expression.js.map +1 -0
  23. package/dist/cjs/in-memory/index.d.ts +2 -0
  24. package/dist/cjs/in-memory/index.js +6 -0
  25. package/dist/cjs/in-memory/index.js.map +1 -0
  26. package/dist/cjs/in-memory/spec.d.ts +23 -0
  27. package/dist/cjs/in-memory/spec.js +141 -0
  28. package/dist/cjs/in-memory/spec.js.map +1 -0
  29. package/dist/cjs/in-memory/store.d.ts +73 -0
  30. package/dist/cjs/in-memory/store.js +267 -0
  31. package/dist/cjs/in-memory/store.js.map +1 -0
  32. package/dist/cjs/in-memory/treap.d.ts +31 -0
  33. package/dist/cjs/in-memory/treap.js +187 -0
  34. package/dist/cjs/in-memory/treap.js.map +1 -0
  35. package/dist/cjs/in-memory/utils.d.ts +10 -0
  36. package/dist/cjs/in-memory/utils.js +38 -0
  37. package/dist/cjs/in-memory/utils.js.map +1 -0
  38. package/dist/cjs/sandbox.js +44 -0
  39. package/dist/cjs/sandbox.js.map +1 -1
  40. package/dist/esm/__test__/client-env-guard.test.d.ts +1 -0
  41. package/dist/esm/__test__/client-env-guard.test.js +26 -0
  42. package/dist/esm/__test__/client-env-guard.test.js.map +1 -0
  43. package/dist/esm/__test__/conformance.test.d.ts +1 -0
  44. package/dist/esm/__test__/conformance.test.js +1833 -0
  45. package/dist/esm/__test__/conformance.test.js.map +1 -0
  46. package/dist/esm/__test__/in-memory.spec.test.d.ts +1 -0
  47. package/dist/esm/__test__/in-memory.spec.test.js +232 -0
  48. package/dist/esm/__test__/in-memory.spec.test.js.map +1 -0
  49. package/dist/esm/client.d.ts +2 -2
  50. package/dist/esm/client.js +14 -6
  51. package/dist/esm/client.js.map +1 -1
  52. package/dist/esm/errors.d.ts +13 -1
  53. package/dist/esm/errors.js +10 -0
  54. package/dist/esm/errors.js.map +1 -1
  55. package/dist/esm/in-memory/document-client.d.ts +45 -0
  56. package/dist/esm/in-memory/document-client.js +791 -0
  57. package/dist/esm/in-memory/document-client.js.map +1 -0
  58. package/dist/esm/in-memory/expression.d.ts +42 -0
  59. package/dist/esm/in-memory/expression.js +657 -0
  60. package/dist/esm/in-memory/expression.js.map +1 -0
  61. package/dist/esm/in-memory/index.d.ts +2 -0
  62. package/dist/esm/in-memory/index.js +3 -0
  63. package/dist/esm/in-memory/index.js.map +1 -0
  64. package/dist/esm/in-memory/spec.d.ts +23 -0
  65. package/dist/esm/in-memory/spec.js +138 -0
  66. package/dist/esm/in-memory/spec.js.map +1 -0
  67. package/dist/esm/in-memory/store.d.ts +73 -0
  68. package/dist/esm/in-memory/store.js +258 -0
  69. package/dist/esm/in-memory/store.js.map +1 -0
  70. package/dist/esm/in-memory/treap.d.ts +31 -0
  71. package/dist/esm/in-memory/treap.js +183 -0
  72. package/dist/esm/in-memory/treap.js.map +1 -0
  73. package/dist/esm/in-memory/utils.d.ts +10 -0
  74. package/dist/esm/in-memory/utils.js +28 -0
  75. package/dist/esm/in-memory/utils.js.map +1 -0
  76. package/dist/esm/sandbox.js +44 -0
  77. package/dist/esm/sandbox.js.map +1 -1
  78. package/package.json +2 -1
  79. package/src/__test__/client-env-guard.test.ts +31 -0
  80. package/src/__test__/conformance.test.ts +2042 -0
  81. package/src/__test__/in-memory.spec.test.ts +283 -0
  82. package/src/client.ts +17 -4
  83. package/src/errors.ts +24 -0
  84. package/src/in-memory/document-client.ts +1140 -0
  85. package/src/in-memory/expression.ts +830 -0
  86. package/src/in-memory/index.ts +2 -0
  87. package/src/in-memory/spec.ts +159 -0
  88. package/src/in-memory/store.ts +360 -0
  89. package/src/in-memory/treap.ts +239 -0
  90. package/src/in-memory/utils.ts +45 -0
  91. package/src/sandbox.ts +56 -0
@@ -0,0 +1,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",
@@ -281,6 +282,61 @@ export interface Sandbox {
281
282
  }
282
283
 
283
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
+
284
340
  const tableName = await createTable()
285
341
 
286
342
  const tracked = createTrackedDocClient(docClient, tableName)