@naturalcycles/db-lib 8.41.1 → 8.42.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 (36) hide show
  1. package/dist/adapter/cachedb/cache.db.d.ts +3 -1
  2. package/dist/adapter/cachedb/cache.db.js +4 -0
  3. package/dist/adapter/file/file.db.js +24 -4
  4. package/dist/adapter/file/file.db.model.d.ts +1 -1
  5. package/dist/adapter/inmemory/inMemory.db.js +23 -14
  6. package/dist/base.common.db.d.ts +1 -0
  7. package/dist/base.common.db.js +1 -0
  8. package/dist/commondao/common.dao.d.ts +8 -1
  9. package/dist/commondao/common.dao.js +55 -3
  10. package/dist/commondao/common.dao.model.d.ts +10 -0
  11. package/dist/db.model.d.ts +2 -0
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.js +1 -2
  14. package/dist/testing/daoTest.js +42 -1
  15. package/dist/testing/dbTest.d.ts +1 -0
  16. package/dist/testing/dbTest.js +43 -11
  17. package/dist/timeseries/commonTimeSeriesDao.js +1 -1
  18. package/dist/transaction/dbTransaction.d.ts +7 -0
  19. package/dist/transaction/dbTransaction.js +24 -2
  20. package/dist/transaction/dbTransaction.util.d.ts +3 -3
  21. package/dist/transaction/dbTransaction.util.js +55 -55
  22. package/package.json +1 -1
  23. package/src/adapter/cachedb/cache.db.ts +7 -1
  24. package/src/adapter/file/file.db.model.ts +1 -1
  25. package/src/adapter/file/file.db.ts +28 -5
  26. package/src/adapter/inmemory/inMemory.db.ts +23 -12
  27. package/src/base.common.db.ts +1 -0
  28. package/src/commondao/common.dao.model.ts +11 -0
  29. package/src/commondao/common.dao.ts +76 -5
  30. package/src/db.model.ts +2 -0
  31. package/src/index.ts +1 -2
  32. package/src/testing/daoTest.ts +54 -1
  33. package/src/testing/dbTest.ts +53 -10
  34. package/src/timeseries/commonTimeSeriesDao.ts +1 -1
  35. package/src/transaction/dbTransaction.ts +26 -1
  36. package/src/transaction/dbTransaction.util.ts +29 -33
@@ -1,76 +1,76 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.commitDBTransactionSimple = exports.mergeDBOperations = void 0;
4
- const js_lib_1 = require("@naturalcycles/js-lib");
3
+ exports.commitDBTransactionSimple = void 0;
5
4
  /**
6
5
  * Optimizes the Transaction (list of DBOperations) to do less operations.
7
6
  * E.g if you save id1 first and then delete it - this function will turn it into a no-op (self-eliminate).
7
+ * UPD: actually, it will only keep delete, but remove previous ops.
8
8
  *
9
9
  * Currently only takes into account SaveBatch and DeleteByIds ops.
10
- * Output ops are maximum 2 (per table) - save and delete (where order actually doesn't matter, cause ids there will not overlap).
10
+ * Output ops are maximum 1 per entity - save or delete.
11
11
  */
12
- function mergeDBOperations(ops) {
13
- if (ops.length <= 1)
14
- return ops; // nothing to optimize there
15
- // This map will be saved in the end. Null would mean "delete"
16
- // saveMap[table][id] => row
17
- const saveMapByTable = {};
18
- // Merge ops using `saveMap`
19
- ops.forEach(op => {
20
- saveMapByTable[op.table] = saveMapByTable[op.table] || {};
21
- if (op.type === 'saveBatch') {
22
- op.rows.forEach(r => (saveMapByTable[op.table][r.id] = r));
23
- }
24
- else if (op.type === 'deleteByIds') {
25
- op.ids.forEach(id => (saveMapByTable[op.table][id] = null));
26
- }
27
- else {
28
- throw new Error(`DBOperation not supported: ${op.type}`);
29
- }
30
- });
31
- const resultOps = [];
32
- (0, js_lib_1._stringMapEntries)(saveMapByTable).forEach(([table, saveMap]) => {
33
- const rowsToSave = [];
34
- const idsToDelete = [];
35
- (0, js_lib_1._stringMapEntries)(saveMap).forEach(([id, r]) => {
36
- if (r === null) {
37
- idsToDelete.push(id);
38
- }
39
- else {
40
- rowsToSave.push(r);
41
- }
42
- });
43
- if (rowsToSave.length) {
44
- resultOps.push({
45
- type: 'saveBatch',
46
- table,
47
- rows: rowsToSave,
48
- });
49
- }
50
- if (idsToDelete.length) {
51
- resultOps.push({
52
- type: 'deleteByIds',
53
- table,
54
- ids: idsToDelete,
55
- });
56
- }
57
- });
58
- return resultOps;
12
+ // Commented out as "overly complicated"
13
+ /*
14
+ export function mergeDBOperations(ops: DBOperation[]): DBOperation[] {
15
+ if (ops.length <= 1) return ops // nothing to optimize there
16
+
17
+ // This map will be saved in the end. Null would mean "delete"
18
+ // saveMap[table][id] => row
19
+ const data: StringMap<StringMap<ObjectWithId | null>> = {}
20
+
21
+ // Merge ops using `saveMap`
22
+ ops.forEach(op => {
23
+ data[op.table] ||= {}
24
+
25
+ if (op.type === 'saveBatch') {
26
+ op.rows.forEach(r => (data[op.table]![r.id] = r))
27
+ } else if (op.type === 'deleteByIds') {
28
+ op.ids.forEach(id => (data[op.table]![id] = null))
29
+ } else {
30
+ throw new Error(`DBOperation not supported: ${(op as any).type}`)
31
+ }
32
+ })
33
+
34
+ const resultOps: DBOperation[] = []
35
+
36
+ _stringMapEntries(data).forEach(([table, map]) => {
37
+ const saveOp: DBSaveBatchOperation = {
38
+ type: 'saveBatch',
39
+ table,
40
+ rows: _stringMapValues(map).filter(_isTruthy),
41
+ }
42
+
43
+ if (saveOp.rows.length) {
44
+ resultOps.push(saveOp)
45
+ }
46
+
47
+ const deleteOp: DBDeleteByIdsOperation = {
48
+ type: 'deleteByIds',
49
+ table,
50
+ ids: _stringMapEntries(map).filter(([id, row]) => row === null).map(([id]) => id),
51
+ }
52
+
53
+ if (deleteOp.ids.length) {
54
+ resultOps.push(deleteOp)
55
+ }
56
+ })
57
+
58
+ return resultOps
59
59
  }
60
- exports.mergeDBOperations = mergeDBOperations;
60
+ */
61
61
  /**
62
62
  * Naive implementation of "Transaction" which just executes all operations one-by-one.
63
63
  * Does NOT actually implement a Transaction, cause partial ops application will happen
64
64
  * in case of an error in the middle.
65
65
  */
66
66
  async function commitDBTransactionSimple(db, tx, opt) {
67
- const ops = mergeDBOperations(tx.ops);
68
- for await (const op of ops) {
67
+ // const ops = mergeDBOperations(tx.ops)
68
+ for await (const op of tx.ops) {
69
69
  if (op.type === 'saveBatch') {
70
- await db.saveBatch(op.table, op.rows, opt);
70
+ await db.saveBatch(op.table, op.rows, { ...op.opt, ...opt });
71
71
  }
72
72
  else if (op.type === 'deleteByIds') {
73
- await db.deleteByIds(op.table, op.ids, opt);
73
+ await db.deleteByIds(op.table, op.ids, { ...op.opt, ...opt });
74
74
  }
75
75
  else {
76
76
  throw new Error(`DBOperation not supported: ${op.type}`);
package/package.json CHANGED
@@ -41,7 +41,7 @@
41
41
  "engines": {
42
42
  "node": ">=14.15"
43
43
  },
44
- "version": "8.41.1",
44
+ "version": "8.42.0",
45
45
  "description": "Lowest Common Denominator API to supported Databases",
46
46
  "keywords": [
47
47
  "db",
@@ -7,8 +7,9 @@ import {
7
7
  } from '@naturalcycles/js-lib'
8
8
  import { BaseCommonDB } from '../../base.common.db'
9
9
  import { CommonDB } from '../../common.db'
10
- import { RunQueryResult } from '../../db.model'
10
+ import { CommonDBOptions, RunQueryResult } from '../../db.model'
11
11
  import { DBQuery } from '../../query/dbQuery'
12
+ import { DBTransaction } from '../../transaction/dbTransaction'
12
13
  import {
13
14
  CacheDBCfg,
14
15
  CacheDBCreateOptions,
@@ -292,4 +293,9 @@ export class CacheDB extends BaseCommonDB implements CommonDB {
292
293
 
293
294
  return deletedIds
294
295
  }
296
+
297
+ override async commitTransaction(tx: DBTransaction, opt?: CommonDBOptions): Promise<void> {
298
+ await this.cfg.downstreamDB.commitTransaction(tx, opt)
299
+ await this.cfg.cacheDB.commitTransaction(tx, opt)
300
+ }
295
301
  }
@@ -6,7 +6,7 @@ export interface FileDBPersistencePlugin {
6
6
  ping(): Promise<void>
7
7
  getTables(): Promise<string[]>
8
8
  loadFile<ROW extends ObjectWithId>(table: string): Promise<ROW[]>
9
- saveFiles(ops: DBSaveBatchOperation[]): Promise<void>
9
+ saveFiles(ops: DBSaveBatchOperation<any>[]): Promise<void>
10
10
  }
11
11
 
12
12
  export interface FileDBCfg {
@@ -12,8 +12,9 @@ import {
12
12
  JsonSchemaRootObject,
13
13
  _filterUndefinedValues,
14
14
  ObjectWithId,
15
- AnyObjectWithId,
16
15
  _assert,
16
+ _deepCopy,
17
+ _stringMapEntries,
17
18
  } from '@naturalcycles/js-lib'
18
19
  import { readableCreate, ReadableTyped } from '@naturalcycles/nodejs-lib'
19
20
  import { dimGrey } from '@naturalcycles/nodejs-lib/dist/colors'
@@ -119,12 +120,19 @@ export class FileDB extends BaseCommonDB implements CommonDB {
119
120
  { concurrency: 16 },
120
121
  )
121
122
 
123
+ const backup = _deepCopy(data)
124
+
122
125
  // 2. Apply ops one by one (in order)
123
126
  tx.ops.forEach(op => {
124
127
  if (op.type === 'deleteByIds') {
125
128
  op.ids.forEach(id => delete data[op.table]![id])
126
129
  } else if (op.type === 'saveBatch') {
127
- op.rows.forEach(r => (data[op.table]![r.id] = r))
130
+ op.rows.forEach(r => {
131
+ if (!r.id) {
132
+ throw new Error('FileDB: row has an empty id')
133
+ }
134
+ data[op.table]![r.id] = r
135
+ })
128
136
  } else {
129
137
  throw new Error(`DBOperation not supported: ${(op as any).type}`)
130
138
  }
@@ -132,16 +140,31 @@ export class FileDB extends BaseCommonDB implements CommonDB {
132
140
 
133
141
  // 3. Sort, turn it into ops
134
142
  // Not filtering empty arrays, cause it's already filtered in this.saveFiles()
135
- const ops: DBSaveBatchOperation[] = Object.keys(data).map(table => {
143
+ const ops: DBSaveBatchOperation[] = _stringMapEntries(data).map(([table, map]) => {
136
144
  return {
137
145
  type: 'saveBatch',
138
146
  table,
139
- rows: this.sortRows(Object.values(data[table]!) as AnyObjectWithId[]),
147
+ rows: this.sortRows(_stringMapValues(map)),
140
148
  }
141
149
  })
142
150
 
143
151
  // 4. Save all files
144
- await this.saveFiles(ops)
152
+ try {
153
+ await this.saveFiles(ops)
154
+ } catch (err) {
155
+ const ops: DBSaveBatchOperation[] = _stringMapEntries(backup).map(([table, map]) => {
156
+ return {
157
+ type: 'saveBatch',
158
+ table,
159
+ rows: this.sortRows(_stringMapValues(map)),
160
+ }
161
+ })
162
+
163
+ // Rollback, ignore rollback error (if any)
164
+ await this.saveFiles(ops).catch(_ => {})
165
+
166
+ throw err
167
+ }
145
168
  }
146
169
 
147
170
  override async runQuery<ROW extends ObjectWithId>(
@@ -12,6 +12,7 @@ import {
12
12
  ObjectWithId,
13
13
  _stringMapValues,
14
14
  CommonLogger,
15
+ _deepCopy,
15
16
  } from '@naturalcycles/js-lib'
16
17
  import {
17
18
  bufferReviver,
@@ -104,13 +105,13 @@ export class InMemoryDB implements CommonDB {
104
105
  async resetCache(_table?: string): Promise<void> {
105
106
  if (_table) {
106
107
  const table = this.cfg.tablesPrefix + _table
107
- this.cfg.logger?.log(`reset ${table}`)
108
+ this.cfg.logger!.log(`reset ${table}`)
108
109
  this.data[table] = {}
109
110
  } else {
110
111
  ;(await this.getTables()).forEach(table => {
111
112
  this.data[table] = {}
112
113
  })
113
- this.cfg.logger?.log('reset')
114
+ this.cfg.logger!.log('reset')
114
115
  }
115
116
  }
116
117
 
@@ -161,7 +162,7 @@ export class InMemoryDB implements CommonDB {
161
162
 
162
163
  rows.forEach(r => {
163
164
  if (!r.id) {
164
- this.cfg.logger?.warn({ rows })
165
+ this.cfg.logger!.warn({ rows })
165
166
  throw new Error(
166
167
  `InMemoryDB doesn't support id auto-generation in saveBatch, row without id was given`,
167
168
  )
@@ -234,14 +235,24 @@ export class InMemoryDB implements CommonDB {
234
235
  }
235
236
 
236
237
  async commitTransaction(tx: DBTransaction, opt?: CommonDBOptions): Promise<void> {
237
- for await (const op of tx.ops) {
238
- if (op.type === 'saveBatch') {
239
- await this.saveBatch(op.table, op.rows, opt)
240
- } else if (op.type === 'deleteByIds') {
241
- await this.deleteByIds(op.table, op.ids, opt)
242
- } else {
243
- throw new Error(`DBOperation not supported: ${(op as any).type}`)
238
+ const backup = _deepCopy(this.data)
239
+
240
+ try {
241
+ for await (const op of tx.ops) {
242
+ if (op.type === 'saveBatch') {
243
+ await this.saveBatch(op.table, op.rows, { ...op.opt, ...opt })
244
+ } else if (op.type === 'deleteByIds') {
245
+ await this.deleteByIds(op.table, op.ids, { ...op.opt, ...opt })
246
+ } else {
247
+ throw new Error(`DBOperation not supported: ${(op as any).type}`)
248
+ }
244
249
  }
250
+ } catch (err) {
251
+ // rollback
252
+ this.data = backup
253
+ this.cfg.logger!.log('InMemoryDB transaction rolled back')
254
+
255
+ throw err
245
256
  }
246
257
  }
247
258
 
@@ -277,7 +288,7 @@ export class InMemoryDB implements CommonDB {
277
288
  ])
278
289
  })
279
290
 
280
- this.cfg.logger?.log(
291
+ this.cfg.logger!.log(
281
292
  `flushToDisk took ${dimGrey(_since(started))} to save ${yellow(tables)} tables`,
282
293
  )
283
294
  }
@@ -319,7 +330,7 @@ export class InMemoryDB implements CommonDB {
319
330
  this.data[table] = _by(rows, r => r.id)
320
331
  })
321
332
 
322
- this.cfg.logger?.log(
333
+ this.cfg.logger!.log(
323
334
  `restoreFromDisk took ${dimGrey(_since(started))} to read ${yellow(files.length)} tables`,
324
335
  )
325
336
  }
@@ -67,6 +67,7 @@ export class BaseCommonDB implements CommonDB {
67
67
 
68
68
  /**
69
69
  * Naive implementation.
70
+ * Doesn't support rollback on error, hence doesn't pass dbTest.
70
71
  * To be extended.
71
72
  */
72
73
  async commitTransaction(tx: DBTransaction, opt?: CommonDBOptions): Promise<void> {
@@ -211,6 +211,17 @@ export interface CommonDaoOptions extends CommonDBOptions {
211
211
  * @experimental
212
212
  */
213
213
  timeout?: number
214
+
215
+ /**
216
+ * If passed - operation will not be performed immediately, but instead "added" to the transaction.
217
+ * In the end - transaction needs to be committed (by calling `commit`).
218
+ * This API is inspired by Datastore API.
219
+ *
220
+ * Only applicable to save* and delete* operations
221
+ *
222
+ * @experimental
223
+ */
224
+ tx?: boolean
214
225
  }
215
226
 
216
227
  /**
@@ -35,8 +35,15 @@ import {
35
35
  writableVoid,
36
36
  } from '@naturalcycles/nodejs-lib'
37
37
  import { DBLibError } from '../cnst'
38
- import { DBModelType, RunQueryResult } from '../db.model'
38
+ import {
39
+ DBDeleteByIdsOperation,
40
+ DBModelType,
41
+ DBOperation,
42
+ DBSaveBatchOperation,
43
+ RunQueryResult,
44
+ } from '../db.model'
39
45
  import { DBQuery, RunnableDBQuery } from '../query/dbQuery'
46
+ import { DBTransaction } from '../transaction/dbTransaction'
40
47
  import {
41
48
  CommonDaoCfg,
42
49
  CommonDaoCreateOptions,
@@ -606,6 +613,57 @@ export class CommonDao<
606
613
  return obj as any
607
614
  }
608
615
 
616
+ tx = {
617
+ save: async (
618
+ bm: Unsaved<BM>,
619
+ opt: CommonDaoSaveOptions<DBM> = {},
620
+ ): Promise<DBSaveBatchOperation> => {
621
+ const row: DBM = (await this.save(bm, { ...opt, tx: true })) as any
622
+
623
+ return {
624
+ type: 'saveBatch',
625
+ table: this.cfg.table,
626
+ rows: [row],
627
+ opt: {
628
+ excludeFromIndexes: this.cfg.excludeFromIndexes as any,
629
+ ...opt,
630
+ },
631
+ }
632
+ },
633
+ saveBatch: async (
634
+ bms: Unsaved<BM>[],
635
+ opt: CommonDaoSaveOptions<DBM> = {},
636
+ ): Promise<DBSaveBatchOperation> => {
637
+ const rows: DBM[] = (await this.saveBatch(bms, { ...opt, tx: true })) as any
638
+
639
+ return {
640
+ type: 'saveBatch',
641
+ table: this.cfg.table,
642
+ rows,
643
+ opt: {
644
+ excludeFromIndexes: this.cfg.excludeFromIndexes as any,
645
+ ...opt,
646
+ },
647
+ }
648
+ },
649
+ deleteByIds: async (ids: ID[], opt: CommonDaoOptions = {}): Promise<DBDeleteByIdsOperation> => {
650
+ return {
651
+ type: 'deleteByIds',
652
+ table: this.cfg.table,
653
+ ids: ids as string[],
654
+ opt,
655
+ }
656
+ },
657
+ deleteById: async (id: ID, opt: CommonDaoOptions = {}): Promise<DBDeleteByIdsOperation> => {
658
+ return {
659
+ type: 'deleteByIds',
660
+ table: this.cfg.table,
661
+ ids: [id as string],
662
+ opt,
663
+ }
664
+ },
665
+ }
666
+
609
667
  // SAVE
610
668
  /**
611
669
  * Mutates with id, created, updated
@@ -615,6 +673,11 @@ export class CommonDao<
615
673
  const idWasGenerated = !bm.id && this.cfg.createId
616
674
  this.assignIdCreatedUpdated(bm, opt) // mutates
617
675
  const dbm = await this.bmToDBM(bm as BM, opt)
676
+
677
+ if (opt.tx) {
678
+ return dbm as any
679
+ }
680
+
618
681
  const table = opt.table || this.cfg.table
619
682
  if (opt.ensureUniqueId && idWasGenerated) await this.ensureUniqueId(table, dbm)
620
683
  if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
@@ -709,6 +772,11 @@ export class CommonDao<
709
772
  const table = opt.table || this.cfg.table
710
773
  bms.forEach(bm => this.assignIdCreatedUpdated(bm, opt))
711
774
  const dbms = await this.bmsToDBM(bms as BM[], opt)
775
+
776
+ if (opt.tx) {
777
+ return dbms as any
778
+ }
779
+
712
780
  if (opt.ensureUniqueId) throw new AppError('ensureUniqueId is not supported in saveBatch')
713
781
  if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
714
782
  opt = { ...opt, saveMethod: 'insert' }
@@ -724,7 +792,6 @@ export class CommonDao<
724
792
  const started = this.logSaveStarted(op, bms, table)
725
793
  const { excludeFromIndexes } = this.cfg
726
794
  const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds
727
-
728
795
  await this.cfg.db.saveBatch(table, dbms, {
729
796
  excludeFromIndexes,
730
797
  assignGeneratedIds,
@@ -1066,9 +1133,13 @@ export class CommonDao<
1066
1133
  await this.cfg.db.ping()
1067
1134
  }
1068
1135
 
1069
- // transaction(): DBTransaction {
1070
- // return this.cfg.db.transaction()
1071
- // }
1136
+ async runInTransaction(ops: Promise<DBOperation>[]): Promise<void> {
1137
+ if (!ops.length) return
1138
+
1139
+ const resolvedOps = await Promise.all(ops)
1140
+
1141
+ await this.cfg.db.commitTransaction(DBTransaction.create(resolvedOps))
1142
+ }
1072
1143
 
1073
1144
  protected logResult(started: number, op: string, res: any, table: string): void {
1074
1145
  if (!this.cfg.logLevel) return
package/src/db.model.ts CHANGED
@@ -56,12 +56,14 @@ export interface DBSaveBatchOperation<ROW extends ObjectWithId = AnyObjectWithId
56
56
  type: 'saveBatch'
57
57
  table: string
58
58
  rows: ROW[]
59
+ opt?: CommonDBSaveOptions<ROW>
59
60
  }
60
61
 
61
62
  export interface DBDeleteByIdsOperation {
62
63
  type: 'deleteByIds'
63
64
  table: string
64
65
  ids: string[]
66
+ opt?: CommonDBOptions
65
67
  }
66
68
 
67
69
  export enum DBRelation {
package/src/index.ts CHANGED
@@ -48,7 +48,7 @@ import {
48
48
  RunnableDBQuery,
49
49
  } from './query/dbQuery'
50
50
  import { DBTransaction, RunnableDBTransaction } from './transaction/dbTransaction'
51
- import { commitDBTransactionSimple, mergeDBOperations } from './transaction/dbTransaction.util'
51
+ import { commitDBTransactionSimple } from './transaction/dbTransaction.util'
52
52
  export * from './kv/commonKeyValueDaoMemoCache'
53
53
 
54
54
  export type {
@@ -104,7 +104,6 @@ export {
104
104
  BaseCommonDB,
105
105
  DBTransaction,
106
106
  RunnableDBTransaction,
107
- mergeDBOperations,
108
107
  commitDBTransactionSimple,
109
108
  CommonKeyValueDao,
110
109
  }
@@ -1,4 +1,4 @@
1
- import { pDelay, _deepCopy, _pick, _sortBy } from '@naturalcycles/js-lib'
1
+ import { pDelay, _deepCopy, _pick, _sortBy, _omit, localTime } from '@naturalcycles/js-lib'
2
2
  import { readableToArray, transformNoOp } from '@naturalcycles/nodejs-lib'
3
3
  import { CommonDaoLogLevel } from '..'
4
4
  import { CommonDB } from '../common.db'
@@ -41,6 +41,7 @@ export function runCommonDaoTest(
41
41
  streaming = true,
42
42
  strongConsistency = true,
43
43
  nullValues = true,
44
+ transactions = true,
44
45
  } = features
45
46
 
46
47
  // const {
@@ -264,4 +265,56 @@ export function runCommonDaoTest(
264
265
  )
265
266
  })
266
267
  }
268
+
269
+ if (transactions) {
270
+ test('transaction happy path', async () => {
271
+ // cleanup
272
+ await dao.query().deleteByQuery()
273
+
274
+ // Test that id, created, updated are created
275
+ const now = localTime().unix()
276
+ await dao.runInTransaction([dao.tx.save(_omit(item1, ['id', 'created', 'updated']))])
277
+
278
+ const loaded = await dao.query().runQuery()
279
+ expect(loaded.length).toBe(1)
280
+ expect(loaded[0]!.id).toBeDefined()
281
+ expect(loaded[0]!.created).toBeGreaterThanOrEqual(now)
282
+ expect(loaded[0]!.updated).toBe(loaded[0]!.created)
283
+
284
+ await dao.runInTransaction([dao.tx.deleteById(loaded[0]!.id)])
285
+
286
+ // saveBatch [item1, 2, 3]
287
+ // save item3 with k1: k1_mod
288
+ // delete item2
289
+ // remaining: item1, item3_with_k1_mod
290
+ await dao.runInTransaction([
291
+ dao.tx.saveBatch(items),
292
+ dao.tx.save({ ...items[2]!, k1: 'k1_mod' }),
293
+ dao.tx.deleteById(items[1]!.id),
294
+ ])
295
+
296
+ const rows = await dao.query().runQuery()
297
+ const expected = [items[0], { ...items[2]!, k1: 'k1_mod' }]
298
+ expectMatch(expected, rows, quirks)
299
+ })
300
+
301
+ test('transaction rollback', async () => {
302
+ await expect(
303
+ dao.runInTransaction([
304
+ dao.tx.deleteById(items[2]!.id),
305
+ dao.tx.save({ ...items[0]!, k1: 5 as any }), // it should fail here
306
+ ]),
307
+ ).rejects.toThrow()
308
+
309
+ const rows = await dao.query().runQuery()
310
+ const expected = [items[0], { ...items[2]!, k1: 'k1_mod' }]
311
+ expectMatch(expected, rows, quirks)
312
+ })
313
+
314
+ if (querying) {
315
+ test('transaction cleanup', async () => {
316
+ await dao.query().deleteByQuery()
317
+ })
318
+ }
319
+ }
267
320
  }