@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.
Files changed (100) 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 +185 -0
  10. package/dist/cjs/__test__/in-memory.spec.test.js.map +1 -0
  11. package/dist/cjs/__test__/rollback.test.d.ts +1 -0
  12. package/dist/cjs/__test__/rollback.test.js +196 -0
  13. package/dist/cjs/__test__/rollback.test.js.map +1 -0
  14. package/dist/cjs/client.d.ts +2 -2
  15. package/dist/cjs/client.js +14 -6
  16. package/dist/cjs/client.js.map +1 -1
  17. package/dist/cjs/errors.d.ts +13 -1
  18. package/dist/cjs/errors.js +12 -1
  19. package/dist/cjs/errors.js.map +1 -1
  20. package/dist/cjs/in-memory/document-client.d.ts +45 -0
  21. package/dist/cjs/in-memory/document-client.js +795 -0
  22. package/dist/cjs/in-memory/document-client.js.map +1 -0
  23. package/dist/cjs/in-memory/expression.d.ts +42 -0
  24. package/dist/cjs/in-memory/expression.js +585 -0
  25. package/dist/cjs/in-memory/expression.js.map +1 -0
  26. package/dist/cjs/in-memory/index.d.ts +2 -0
  27. package/dist/cjs/in-memory/index.js +6 -0
  28. package/dist/cjs/in-memory/index.js.map +1 -0
  29. package/dist/cjs/in-memory/spec.d.ts +23 -0
  30. package/dist/cjs/in-memory/spec.js +141 -0
  31. package/dist/cjs/in-memory/spec.js.map +1 -0
  32. package/dist/cjs/in-memory/store.d.ts +73 -0
  33. package/dist/cjs/in-memory/store.js +267 -0
  34. package/dist/cjs/in-memory/store.js.map +1 -0
  35. package/dist/cjs/in-memory/treap.d.ts +31 -0
  36. package/dist/cjs/in-memory/treap.js +187 -0
  37. package/dist/cjs/in-memory/treap.js.map +1 -0
  38. package/dist/cjs/in-memory/utils.d.ts +10 -0
  39. package/dist/cjs/in-memory/utils.js +38 -0
  40. package/dist/cjs/in-memory/utils.js.map +1 -0
  41. package/dist/cjs/sandbox.d.ts +2 -0
  42. package/dist/cjs/sandbox.js +172 -1
  43. package/dist/cjs/sandbox.js.map +1 -1
  44. package/dist/esm/__test__/client-env-guard.test.d.ts +1 -0
  45. package/dist/esm/__test__/client-env-guard.test.js +26 -0
  46. package/dist/esm/__test__/client-env-guard.test.js.map +1 -0
  47. package/dist/esm/__test__/conformance.test.d.ts +1 -0
  48. package/dist/esm/__test__/conformance.test.js +1833 -0
  49. package/dist/esm/__test__/conformance.test.js.map +1 -0
  50. package/dist/esm/__test__/in-memory.spec.test.d.ts +1 -0
  51. package/dist/esm/__test__/in-memory.spec.test.js +183 -0
  52. package/dist/esm/__test__/in-memory.spec.test.js.map +1 -0
  53. package/dist/esm/__test__/rollback.test.d.ts +1 -0
  54. package/dist/esm/__test__/rollback.test.js +194 -0
  55. package/dist/esm/__test__/rollback.test.js.map +1 -0
  56. package/dist/esm/client.d.ts +2 -2
  57. package/dist/esm/client.js +14 -6
  58. package/dist/esm/client.js.map +1 -1
  59. package/dist/esm/errors.d.ts +13 -1
  60. package/dist/esm/errors.js +10 -0
  61. package/dist/esm/errors.js.map +1 -1
  62. package/dist/esm/in-memory/document-client.d.ts +45 -0
  63. package/dist/esm/in-memory/document-client.js +791 -0
  64. package/dist/esm/in-memory/document-client.js.map +1 -0
  65. package/dist/esm/in-memory/expression.d.ts +42 -0
  66. package/dist/esm/in-memory/expression.js +577 -0
  67. package/dist/esm/in-memory/expression.js.map +1 -0
  68. package/dist/esm/in-memory/index.d.ts +2 -0
  69. package/dist/esm/in-memory/index.js +3 -0
  70. package/dist/esm/in-memory/index.js.map +1 -0
  71. package/dist/esm/in-memory/spec.d.ts +23 -0
  72. package/dist/esm/in-memory/spec.js +138 -0
  73. package/dist/esm/in-memory/spec.js.map +1 -0
  74. package/dist/esm/in-memory/store.d.ts +73 -0
  75. package/dist/esm/in-memory/store.js +258 -0
  76. package/dist/esm/in-memory/store.js.map +1 -0
  77. package/dist/esm/in-memory/treap.d.ts +31 -0
  78. package/dist/esm/in-memory/treap.js +183 -0
  79. package/dist/esm/in-memory/treap.js.map +1 -0
  80. package/dist/esm/in-memory/utils.d.ts +10 -0
  81. package/dist/esm/in-memory/utils.js +28 -0
  82. package/dist/esm/in-memory/utils.js.map +1 -0
  83. package/dist/esm/sandbox.d.ts +2 -0
  84. package/dist/esm/sandbox.js +172 -1
  85. package/dist/esm/sandbox.js.map +1 -1
  86. package/package.json +2 -1
  87. package/src/__test__/client-env-guard.test.ts +31 -0
  88. package/src/__test__/conformance.test.ts +2042 -0
  89. package/src/__test__/in-memory.spec.test.ts +230 -0
  90. package/src/__test__/rollback.test.ts +279 -0
  91. package/src/client.ts +17 -4
  92. package/src/errors.ts +24 -0
  93. package/src/in-memory/document-client.ts +1140 -0
  94. package/src/in-memory/expression.ts +730 -0
  95. package/src/in-memory/index.ts +2 -0
  96. package/src/in-memory/spec.ts +159 -0
  97. package/src/in-memory/store.ts +360 -0
  98. package/src/in-memory/treap.ts +239 -0
  99. package/src/in-memory/utils.ts +45 -0
  100. 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
- client.setDocumentClient(docClient)
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
  }