@naturalcycles/db-lib 8.41.0 → 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 +59 -6
- 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 +2 -3
- 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 +80 -8
- 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
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
"@naturalcycles/bench-lib": "^1.0.0",
|
|
13
13
|
"@naturalcycles/dev-lib": "^13.0.0",
|
|
14
14
|
"@types/node": "^18.0.3",
|
|
15
|
-
"jest": "^29.0.0"
|
|
16
|
-
"weak-napi": "^2.0.2"
|
|
15
|
+
"jest": "^29.0.0"
|
|
17
16
|
},
|
|
18
17
|
"files": [
|
|
19
18
|
"dist",
|
|
@@ -42,7 +41,7 @@
|
|
|
42
41
|
"engines": {
|
|
43
42
|
"node": ">=14.15"
|
|
44
43
|
},
|
|
45
|
-
"version": "8.
|
|
44
|
+
"version": "8.42.0",
|
|
46
45
|
"description": "Lowest Common Denominator API to supported Databases",
|
|
47
46
|
"keywords": [
|
|
48
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 =>
|
|
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,
|
|
@@ -106,9 +113,10 @@ export class CommonDao<
|
|
|
106
113
|
|
|
107
114
|
// CREATE
|
|
108
115
|
create(part: Partial<BM> = {}, opt: CommonDaoOptions = {}): Saved<BM> {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
const bm = this.cfg.hooks!.beforeCreate!(part)
|
|
117
|
+
// First assignIdCreatedUpdated, then validate!
|
|
118
|
+
this.assignIdCreatedUpdated(bm as any, opt)
|
|
119
|
+
return this.validateAndConvert(bm, this.cfg.bmSchema, DBModelType.BM, opt)
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
// GET
|
|
@@ -605,6 +613,57 @@ export class CommonDao<
|
|
|
605
613
|
return obj as any
|
|
606
614
|
}
|
|
607
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
|
+
|
|
608
667
|
// SAVE
|
|
609
668
|
/**
|
|
610
669
|
* Mutates with id, created, updated
|
|
@@ -614,6 +673,11 @@ export class CommonDao<
|
|
|
614
673
|
const idWasGenerated = !bm.id && this.cfg.createId
|
|
615
674
|
this.assignIdCreatedUpdated(bm, opt) // mutates
|
|
616
675
|
const dbm = await this.bmToDBM(bm as BM, opt)
|
|
676
|
+
|
|
677
|
+
if (opt.tx) {
|
|
678
|
+
return dbm as any
|
|
679
|
+
}
|
|
680
|
+
|
|
617
681
|
const table = opt.table || this.cfg.table
|
|
618
682
|
if (opt.ensureUniqueId && idWasGenerated) await this.ensureUniqueId(table, dbm)
|
|
619
683
|
if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
|
|
@@ -708,6 +772,11 @@ export class CommonDao<
|
|
|
708
772
|
const table = opt.table || this.cfg.table
|
|
709
773
|
bms.forEach(bm => this.assignIdCreatedUpdated(bm, opt))
|
|
710
774
|
const dbms = await this.bmsToDBM(bms as BM[], opt)
|
|
775
|
+
|
|
776
|
+
if (opt.tx) {
|
|
777
|
+
return dbms as any
|
|
778
|
+
}
|
|
779
|
+
|
|
711
780
|
if (opt.ensureUniqueId) throw new AppError('ensureUniqueId is not supported in saveBatch')
|
|
712
781
|
if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
|
|
713
782
|
opt = { ...opt, saveMethod: 'insert' }
|
|
@@ -723,7 +792,6 @@ export class CommonDao<
|
|
|
723
792
|
const started = this.logSaveStarted(op, bms, table)
|
|
724
793
|
const { excludeFromIndexes } = this.cfg
|
|
725
794
|
const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds
|
|
726
|
-
|
|
727
795
|
await this.cfg.db.saveBatch(table, dbms, {
|
|
728
796
|
excludeFromIndexes,
|
|
729
797
|
assignGeneratedIds,
|
|
@@ -1065,9 +1133,13 @@ export class CommonDao<
|
|
|
1065
1133
|
await this.cfg.db.ping()
|
|
1066
1134
|
}
|
|
1067
1135
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
+
}
|
|
1071
1143
|
|
|
1072
1144
|
protected logResult(started: number, op: string, res: any, table: string): void {
|
|
1073
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
|
}
|