@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.
- package/dist/adapter/cachedb/cache.db.d.ts +3 -1
- package/dist/adapter/cachedb/cache.db.js +4 -0
- package/dist/adapter/file/file.db.js +24 -4
- package/dist/adapter/file/file.db.model.d.ts +1 -1
- package/dist/adapter/inmemory/inMemory.db.js +23 -14
- package/dist/base.common.db.d.ts +1 -0
- package/dist/base.common.db.js +1 -0
- package/dist/commondao/common.dao.d.ts +8 -1
- package/dist/commondao/common.dao.js +55 -3
- package/dist/commondao/common.dao.model.d.ts +10 -0
- package/dist/db.model.d.ts +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -2
- package/dist/testing/daoTest.js +42 -1
- package/dist/testing/dbTest.d.ts +1 -0
- package/dist/testing/dbTest.js +43 -11
- package/dist/timeseries/commonTimeSeriesDao.js +1 -1
- package/dist/transaction/dbTransaction.d.ts +7 -0
- package/dist/transaction/dbTransaction.js +24 -2
- package/dist/transaction/dbTransaction.util.d.ts +3 -3
- package/dist/transaction/dbTransaction.util.js +55 -55
- package/package.json +1 -1
- package/src/adapter/cachedb/cache.db.ts +7 -1
- package/src/adapter/file/file.db.model.ts +1 -1
- package/src/adapter/file/file.db.ts +28 -5
- package/src/adapter/inmemory/inMemory.db.ts +23 -12
- package/src/base.common.db.ts +1 -0
- package/src/commondao/common.dao.model.ts +11 -0
- package/src/commondao/common.dao.ts +76 -5
- package/src/db.model.ts +2 -0
- package/src/index.ts +1 -2
- package/src/testing/daoTest.ts +54 -1
- package/src/testing/dbTest.ts +53 -10
- package/src/timeseries/commonTimeSeriesDao.ts +1 -1
- package/src/transaction/dbTransaction.ts +26 -1
- 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 =
|
|
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
|
|
10
|
+
* Output ops are maximum 1 per entity - save or delete.
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
@@ -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 =>
|
|
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[] =
|
|
143
|
+
const ops: DBSaveBatchOperation[] = _stringMapEntries(data).map(([table, map]) => {
|
|
136
144
|
return {
|
|
137
145
|
type: 'saveBatch',
|
|
138
146
|
table,
|
|
139
|
-
rows: this.sortRows(
|
|
147
|
+
rows: this.sortRows(_stringMapValues(map)),
|
|
140
148
|
}
|
|
141
149
|
})
|
|
142
150
|
|
|
143
151
|
// 4. Save all files
|
|
144
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
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
|
|
333
|
+
this.cfg.logger!.log(
|
|
323
334
|
`restoreFromDisk took ${dimGrey(_since(started))} to read ${yellow(files.length)} tables`,
|
|
324
335
|
)
|
|
325
336
|
}
|
package/src/base.common.db.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
1070
|
-
|
|
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
|
|
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
|
}
|
package/src/testing/daoTest.ts
CHANGED
|
@@ -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
|
}
|