@naturalcycles/db-lib 9.0.0 → 9.1.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.
@@ -1,6 +1,6 @@
1
1
  import { JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib';
2
2
  import { ReadableTyped } from '@naturalcycles/nodejs-lib';
3
- import { BaseCommonDB, CommonDBSupport, DBOperation, DBSaveBatchOperation, DBTransaction } from '../..';
3
+ import { BaseCommonDB, CommonDBSupport, DBSaveBatchOperation } from '../..';
4
4
  import { CommonDB } from '../../common.db';
5
5
  import { CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, RunQueryResult } from '../../db.model';
6
6
  import { DBQuery } from '../../query/dbQuery';
@@ -32,18 +32,7 @@ export declare class FileDB extends BaseCommonDB implements CommonDB {
32
32
  loadFile<ROW extends ObjectWithId>(table: string): Promise<ROW[]>;
33
33
  saveFile<ROW extends ObjectWithId>(table: string, _rows: ROW[]): Promise<void>;
34
34
  saveFiles<ROW extends ObjectWithId>(ops: DBSaveBatchOperation<ROW>[]): Promise<void>;
35
- createTransaction(): Promise<FileDBTransaction>;
36
35
  sortRows<ROW extends ObjectWithId>(rows: ROW[]): ROW[];
37
36
  private logStarted;
38
37
  private logFinished;
39
38
  }
40
- export declare class FileDBTransaction implements DBTransaction {
41
- private db;
42
- constructor(db: FileDB);
43
- ops: DBOperation[];
44
- /**
45
- * Implementation is optimized for loading/saving _whole files_.
46
- */
47
- commit(): Promise<void>;
48
- rollback(): Promise<void>;
49
- }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FileDBTransaction = exports.FileDB = void 0;
3
+ exports.FileDB = void 0;
4
4
  const js_lib_1 = require("@naturalcycles/js-lib");
5
5
  const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
6
6
  const __1 = require("../..");
@@ -24,7 +24,7 @@ class FileDB extends __1.BaseCommonDB {
24
24
  updateSaveMethod: false,
25
25
  updateByQuery: false,
26
26
  createTable: false,
27
- transactions: false,
27
+ transactions: false, // todo
28
28
  };
29
29
  this.cfg = {
30
30
  sortObjects: true,
@@ -140,9 +140,9 @@ class FileDB extends __1.BaseCommonDB {
140
140
  await this.cfg.plugin.saveFiles(ops);
141
141
  this.logFinished(started, op);
142
142
  }
143
- async createTransaction() {
144
- return new FileDBTransaction(this);
145
- }
143
+ // override async createTransaction(): Promise<FileDBTransaction> {
144
+ // return new FileDBTransaction(this)
145
+ // }
146
146
  sortRows(rows) {
147
147
  rows = rows.map(r => (0, js_lib_1._filterUndefinedValues)(r));
148
148
  if (this.cfg.sortOnSave) {
@@ -168,69 +168,79 @@ class FileDB extends __1.BaseCommonDB {
168
168
  }
169
169
  }
170
170
  exports.FileDB = FileDB;
171
- class FileDBTransaction {
172
- constructor(db) {
173
- this.db = db;
174
- this.ops = [];
175
- }
176
- /**
177
- * Implementation is optimized for loading/saving _whole files_.
178
- */
179
- async commit() {
180
- // data[table][id] => row
181
- const data = {};
182
- // 1. Load all tables data (concurrently)
183
- const tables = (0, js_lib_1._uniq)(this.ops.map(o => o.table));
184
- await (0, js_lib_1.pMap)(tables, async (table) => {
185
- const rows = await this.db.loadFile(table);
186
- data[table] = (0, js_lib_1._by)(rows, r => r.id);
187
- }, { concurrency: 16 });
188
- const backup = (0, js_lib_1._deepCopy)(data);
189
- // 2. Apply ops one by one (in order)
190
- this.ops.forEach(op => {
191
- if (op.type === 'deleteByIds') {
192
- op.ids.forEach(id => delete data[op.table][id]);
193
- }
194
- else if (op.type === 'saveBatch') {
195
- op.rows.forEach(r => {
196
- if (!r.id) {
197
- throw new Error('FileDB: row has an empty id');
198
- }
199
- data[op.table][r.id] = r;
200
- });
201
- }
202
- else {
203
- throw new Error(`DBOperation not supported: ${op.type}`);
204
- }
205
- });
206
- // 3. Sort, turn it into ops
207
- // Not filtering empty arrays, cause it's already filtered in this.saveFiles()
208
- const ops = (0, js_lib_1._stringMapEntries)(data).map(([table, map]) => {
209
- return {
210
- type: 'saveBatch',
211
- table,
212
- rows: this.db.sortRows((0, js_lib_1._stringMapValues)(map)),
213
- };
214
- });
215
- // 4. Save all files
216
- try {
217
- await this.db.saveFiles(ops);
218
- }
219
- catch (err) {
220
- const ops = (0, js_lib_1._stringMapEntries)(backup).map(([table, map]) => {
221
- return {
222
- type: 'saveBatch',
223
- table,
224
- rows: this.db.sortRows((0, js_lib_1._stringMapValues)(map)),
225
- };
226
- });
227
- // Rollback, ignore rollback error (if any)
228
- await this.db.saveFiles(ops).catch(_ => { });
229
- throw err;
171
+ // todo: get back and fix it
172
+ // Implementation is optimized for loading/saving _whole files_.
173
+ /*
174
+ export class FileDBTransaction implements DBTransaction {
175
+ constructor(private db: FileDB) {}
176
+
177
+ ops: DBOperation[] = []
178
+
179
+ async commit(): Promise<void> {
180
+ // data[table][id] => row
181
+ const data: StringMap<StringMap<ObjectWithId>> = {}
182
+
183
+ // 1. Load all tables data (concurrently)
184
+ const tables = _uniq(this.ops.map(o => o.table))
185
+
186
+ await pMap(
187
+ tables,
188
+ async table => {
189
+ const rows = await this.db.loadFile(table)
190
+ data[table] = _by(rows, r => r.id)
191
+ },
192
+ { concurrency: 16 },
193
+ )
194
+
195
+ const backup = _deepCopy(data)
196
+
197
+ // 2. Apply ops one by one (in order)
198
+ this.ops.forEach(op => {
199
+ if (op.type === 'deleteByIds') {
200
+ op.ids.forEach(id => delete data[op.table]![id])
201
+ } else if (op.type === 'saveBatch') {
202
+ op.rows.forEach(r => {
203
+ if (!r.id) {
204
+ throw new Error('FileDB: row has an empty id')
205
+ }
206
+ data[op.table]![r.id] = r
207
+ })
208
+ } else {
209
+ throw new Error(`DBOperation not supported: ${(op as any).type}`)
210
+ }
211
+ })
212
+
213
+ // 3. Sort, turn it into ops
214
+ // Not filtering empty arrays, cause it's already filtered in this.saveFiles()
215
+ const ops: DBSaveBatchOperation[] = _stringMapEntries(data).map(([table, map]) => {
216
+ return {
217
+ type: 'saveBatch',
218
+ table,
219
+ rows: this.db.sortRows(_stringMapValues(map)),
220
+ }
221
+ })
222
+
223
+ // 4. Save all files
224
+ try {
225
+ await this.db.saveFiles(ops)
226
+ } catch (err) {
227
+ const ops: DBSaveBatchOperation[] = _stringMapEntries(backup).map(([table, map]) => {
228
+ return {
229
+ type: 'saveBatch',
230
+ table,
231
+ rows: this.db.sortRows(_stringMapValues(map)),
230
232
  }
231
- }
232
- async rollback() {
233
- this.ops = [];
234
- }
233
+ })
234
+
235
+ // Rollback, ignore rollback error (if any)
236
+ await this.db.saveFiles(ops).catch(_ => {})
237
+
238
+ throw err
239
+ }
240
+ }
241
+
242
+ async rollback(): Promise<void> {
243
+ this.ops = []
244
+ }
235
245
  }
236
- exports.FileDBTransaction = FileDBTransaction;
246
+ */
@@ -1,6 +1,6 @@
1
1
  import { JsonSchemaObject, StringMap, JsonSchemaRootObject, ObjectWithId, CommonLogger } from '@naturalcycles/js-lib';
2
2
  import { ReadableTyped } from '@naturalcycles/nodejs-lib';
3
- import { CommonDB, CommonDBType, DBOperation, DBPatch } from '../..';
3
+ import { CommonDB, CommonDBType, DBOperation, DBPatch, DBTransactionFn } from '../..';
4
4
  import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions, DBTransaction, RunQueryResult } from '../../db.model';
5
5
  import { DBQuery } from '../../query/dbQuery';
6
6
  export interface InMemoryDBCfg {
@@ -72,13 +72,13 @@ export declare class InMemoryDB implements CommonDB {
72
72
  createTable<ROW extends ObjectWithId>(_table: string, _schema: JsonSchemaObject<ROW>, opt?: CommonDBCreateOptions): Promise<void>;
73
73
  getByIds<ROW extends ObjectWithId>(_table: string, ids: ROW['id'][], _opt?: CommonDBOptions): Promise<ROW[]>;
74
74
  saveBatch<ROW extends Partial<ObjectWithId>>(_table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
75
- deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions): Promise<number>;
76
- deleteByIds(_table: string, ids: string[], opt?: CommonDBOptions): Promise<number>;
75
+ deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<number>;
76
+ deleteByIds(_table: string, ids: string[], _opt?: CommonDBOptions): Promise<number>;
77
77
  updateByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, patch: DBPatch<ROW>): Promise<number>;
78
78
  runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<RunQueryResult<ROW>>;
79
79
  runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<number>;
80
80
  streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): ReadableTyped<ROW>;
81
- createTransaction(): Promise<DBTransaction>;
81
+ runInTransaction(fn: DBTransactionFn): Promise<void>;
82
82
  /**
83
83
  * Flushes all tables (all namespaces) at once.
84
84
  */
@@ -92,6 +92,9 @@ export declare class InMemoryDBTransaction implements DBTransaction {
92
92
  private db;
93
93
  constructor(db: InMemoryDB);
94
94
  ops: DBOperation[];
95
+ getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: CommonDBOptions): Promise<ROW[]>;
96
+ saveBatch<ROW extends Partial<ObjectWithId>>(table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
97
+ deleteByIds(table: string, ids: string[], opt?: CommonDBOptions): Promise<number>;
95
98
  commit(): Promise<void>;
96
99
  rollback(): Promise<void>;
97
100
  }
@@ -77,17 +77,6 @@ class InMemoryDB {
77
77
  return ids.map(id => this.data[table][id]).filter(Boolean);
78
78
  }
79
79
  async saveBatch(_table, rows, opt = {}) {
80
- const { tx } = opt;
81
- if (tx) {
82
- ;
83
- tx.ops.push({
84
- type: 'saveBatch',
85
- table: _table,
86
- rows,
87
- opt: (0, js_lib_1._omit)(opt, ['tx']),
88
- });
89
- return;
90
- }
91
80
  const table = this.cfg.tablesPrefix + _table;
92
81
  this.data[table] ||= {};
93
82
  rows.forEach(r => {
@@ -107,39 +96,17 @@ class InMemoryDB {
107
96
  this.data[table][r.id] = JSON.parse(JSON.stringify(r), nodejs_lib_1.bufferReviver);
108
97
  });
109
98
  }
110
- async deleteByQuery(q, opt = {}) {
99
+ async deleteByQuery(q, _opt) {
111
100
  const table = this.cfg.tablesPrefix + q.table;
112
101
  if (!this.data[table])
113
102
  return 0;
114
103
  const ids = (0, __1.queryInMemory)(q, Object.values(this.data[table])).map(r => r.id);
115
- const { tx } = opt;
116
- if (tx) {
117
- ;
118
- tx.ops.push({
119
- type: 'deleteByIds',
120
- table: q.table,
121
- ids,
122
- opt: (0, js_lib_1._omit)(opt, ['tx']),
123
- });
124
- return ids.length;
125
- }
126
104
  return await this.deleteByIds(q.table, ids);
127
105
  }
128
- async deleteByIds(_table, ids, opt = {}) {
106
+ async deleteByIds(_table, ids, _opt) {
129
107
  const table = this.cfg.tablesPrefix + _table;
130
108
  if (!this.data[table])
131
109
  return 0;
132
- const { tx } = opt;
133
- if (tx) {
134
- ;
135
- tx.ops.push({
136
- type: 'deleteByIds',
137
- table: _table,
138
- ids,
139
- opt: (0, js_lib_1._omit)(opt, ['tx']),
140
- });
141
- return ids.length;
142
- }
143
110
  let count = 0;
144
111
  ids.forEach(id => {
145
112
  if (!this.data[table][id])
@@ -153,7 +120,6 @@ class InMemoryDB {
153
120
  const patchEntries = Object.entries(patch);
154
121
  if (!patchEntries.length)
155
122
  return 0;
156
- // todo: can we support tx here? :thinking:
157
123
  const table = this.cfg.tablesPrefix + q.table;
158
124
  const rows = (0, __1.queryInMemory)(q, Object.values(this.data[table] || {}));
159
125
  rows.forEach((row) => {
@@ -180,8 +146,16 @@ class InMemoryDB {
180
146
  const table = this.cfg.tablesPrefix + q.table;
181
147
  return node_stream_1.Readable.from((0, __1.queryInMemory)(q, Object.values(this.data[table] || {})));
182
148
  }
183
- async createTransaction() {
184
- return new InMemoryDBTransaction(this);
149
+ async runInTransaction(fn) {
150
+ const tx = new InMemoryDBTransaction(this);
151
+ try {
152
+ await fn(tx);
153
+ await tx.commit();
154
+ }
155
+ catch (err) {
156
+ await tx.rollback();
157
+ throw err;
158
+ }
185
159
  }
186
160
  /**
187
161
  * Flushes all tables (all namespaces) at once.
@@ -243,6 +217,26 @@ class InMemoryDBTransaction {
243
217
  this.db = db;
244
218
  this.ops = [];
245
219
  }
220
+ async getByIds(table, ids, opt) {
221
+ return await this.db.getByIds(table, ids, opt);
222
+ }
223
+ async saveBatch(table, rows, opt) {
224
+ this.ops.push({
225
+ type: 'saveBatch',
226
+ table,
227
+ rows,
228
+ opt,
229
+ });
230
+ }
231
+ async deleteByIds(table, ids, opt) {
232
+ this.ops.push({
233
+ type: 'deleteByIds',
234
+ table,
235
+ ids,
236
+ opt,
237
+ });
238
+ return ids.length;
239
+ }
246
240
  async commit() {
247
241
  const backup = (0, js_lib_1._deepCopy)(this.db.data);
248
242
  try {
@@ -261,6 +255,7 @@ class InMemoryDBTransaction {
261
255
  }
262
256
  catch (err) {
263
257
  // rollback
258
+ this.ops = [];
264
259
  this.db.data = backup;
265
260
  this.db.cfg.logger.log('InMemoryDB transaction rolled back');
266
261
  throw err;
@@ -1,7 +1,7 @@
1
1
  import { JsonSchemaObject, JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib';
2
2
  import { ReadableTyped } from '@naturalcycles/nodejs-lib';
3
3
  import { CommonDB, CommonDBSupport, CommonDBType } from './common.db';
4
- import { CommonDBOptions, CommonDBSaveOptions, DBPatch, DBTransaction, RunQueryResult } from './db.model';
4
+ import { CommonDBOptions, CommonDBSaveOptions, DBPatch, DBTransactionFn, RunQueryResult } from './db.model';
5
5
  import { DBQuery } from './query/dbQuery';
6
6
  /**
7
7
  * No-op implementation of CommonDB interface.
@@ -22,5 +22,5 @@ export declare class BaseCommonDB implements CommonDB {
22
22
  saveBatch<ROW extends Partial<ObjectWithId>>(table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
23
23
  streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>): ReadableTyped<ROW>;
24
24
  deleteByIds(table: string, ids: string[], opt?: CommonDBOptions): Promise<number>;
25
- createTransaction(): Promise<DBTransaction>;
25
+ runInTransaction(fn: DBTransactionFn): Promise<void>;
26
26
  }
@@ -49,8 +49,10 @@ class BaseCommonDB {
49
49
  async deleteByIds(table, ids, opt) {
50
50
  throw new Error('deleteByIds is not implemented');
51
51
  }
52
- async createTransaction() {
53
- return new dbTransaction_util_1.FakeDBTransaction(this);
52
+ async runInTransaction(fn) {
53
+ const tx = new dbTransaction_util_1.FakeDBTransaction(this);
54
+ await fn(tx);
55
+ // there's no try/catch and rollback, as there's nothing to rollback
54
56
  }
55
57
  }
56
58
  exports.BaseCommonDB = BaseCommonDB;
@@ -1,6 +1,6 @@
1
1
  import { JsonSchemaObject, JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib';
2
- import { ReadableTyped } from '@naturalcycles/nodejs-lib';
3
- import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, DBPatch, DBTransaction, RunQueryResult } from './db.model';
2
+ import type { ReadableTyped } from '@naturalcycles/nodejs-lib';
3
+ import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, DBPatch, DBTransactionFn, RunQueryResult } from './db.model';
4
4
  import { DBQuery } from './query/dbQuery';
5
5
  export declare enum CommonDBType {
6
6
  'document' = "document",
@@ -100,8 +100,12 @@ export interface CommonDB {
100
100
  /**
101
101
  * Should be implemented as a Transaction (best effort), which means that
102
102
  * either ALL or NONE of the operations should be applied.
103
+ *
104
+ * Transaction is automatically committed if fn resolves normally.
105
+ * Transaction is rolled back if fn throws, the error is re-thrown in that case.
106
+ * Graceful rollback is allowed on tx.rollback()
103
107
  */
104
- createTransaction: () => Promise<DBTransaction>;
108
+ runInTransaction: (fn: DBTransactionFn) => Promise<void>;
105
109
  }
106
110
  /**
107
111
  * Manifest of supported features.
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  import { Transform } from 'node:stream';
3
- import { AnyObject, AsyncMapper, JsonSchemaObject, JsonSchemaRootObject, ObjectWithId, Saved, UnixTimestampMillisNumber, Unsaved, ZodSchema } from '@naturalcycles/js-lib';
3
+ import { AnyObject, AsyncMapper, CommonLogger, JsonSchemaObject, JsonSchemaRootObject, ObjectWithId, Saved, UnixTimestampMillisNumber, Unsaved, ZodSchema } from '@naturalcycles/js-lib';
4
4
  import { AjvSchema, ObjectSchema, ReadableTyped } from '@naturalcycles/nodejs-lib';
5
5
  import { DBModelType, DBPatch, DBTransaction, RunQueryResult } from '../db.model';
6
6
  import { DBQuery, RunnableDBQuery } from '../query/dbQuery';
@@ -169,23 +169,34 @@ export declare class CommonDao<BM extends Partial<ObjectWithId>, DBM extends Obj
169
169
  * Proxy to this.cfg.db.ping
170
170
  */
171
171
  ping(): Promise<void>;
172
- useTransaction(fn: (tx: CommonDaoTransaction) => Promise<void>): Promise<void>;
173
- createTransaction(): Promise<CommonDaoTransaction>;
172
+ runInTransaction(fn: CommonDaoTransactionFn): Promise<void>;
174
173
  protected logResult(started: number, op: string, res: any, table: string): void;
175
174
  protected logSaveResult(started: number, op: string, table: string): void;
176
175
  protected logStarted(op: string, table: string, force?: boolean): UnixTimestampMillisNumber;
177
176
  protected logSaveStarted(op: string, items: any, table: string): UnixTimestampMillisNumber;
178
177
  }
178
+ /**
179
+ * Transaction is committed when the function returns resolved Promise (aka "returns normally").
180
+ *
181
+ * Transaction is rolled back when the function returns rejected Promise (aka "throws").
182
+ */
183
+ export type CommonDaoTransactionFn = (tx: CommonDaoTransaction) => Promise<void>;
184
+ /**
185
+ * Transaction context.
186
+ * Has similar API than CommonDao, but all operations are performed in the context of the transaction.
187
+ */
179
188
  export declare class CommonDaoTransaction {
180
189
  private tx;
181
- constructor(tx: DBTransaction);
182
- commit(): Promise<void>;
190
+ private logger;
191
+ constructor(tx: DBTransaction, logger: CommonLogger);
192
+ /**
193
+ * Perform a graceful rollback without throwing/re-throwing any error.
194
+ */
183
195
  rollback(): Promise<void>;
184
- getById<BM extends Partial<ObjectWithId>, DBM extends ObjectWithId>(dao: CommonDao<BM, DBM, any>, id: string, opt?: CommonDaoOptions): Promise<Saved<BM> | null>;
196
+ getById<BM extends Partial<ObjectWithId>, DBM extends ObjectWithId>(dao: CommonDao<BM, DBM, any>, id?: string | null, opt?: CommonDaoOptions): Promise<Saved<BM> | null>;
185
197
  getByIds<BM extends Partial<ObjectWithId>, DBM extends ObjectWithId>(dao: CommonDao<BM, DBM, any>, ids: string[], opt?: CommonDaoOptions): Promise<Saved<BM>[]>;
186
- runQuery<BM extends Partial<ObjectWithId>, DBM extends ObjectWithId>(dao: CommonDao<BM, DBM, any>, q: DBQuery<DBM>, opt?: CommonDaoOptions): Promise<Saved<BM>[]>;
187
198
  save<BM extends Partial<ObjectWithId>, DBM extends ObjectWithId>(dao: CommonDao<BM, DBM, any>, bm: Unsaved<BM>, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>>;
188
199
  saveBatch<BM extends Partial<ObjectWithId>, DBM extends ObjectWithId>(dao: CommonDao<BM, DBM, any>, bms: Unsaved<BM>[], opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>[]>;
189
- deleteById(dao: CommonDao<any>, id: string, opt?: CommonDaoOptions): Promise<number>;
200
+ deleteById(dao: CommonDao<any>, id?: string | null, opt?: CommonDaoOptions): Promise<number>;
190
201
  deleteByIds(dao: CommonDao<any>, ids: string[], opt?: CommonDaoOptions): Promise<number>;
191
202
  }
@@ -124,7 +124,7 @@ class CommonDao {
124
124
  const op = `getByIds ${ids.length} id(s) (${(0, js_lib_1._truncate)(ids.slice(0, 10).join(', '), 50)})`;
125
125
  const table = opt.table || this.cfg.table;
126
126
  const started = this.logStarted(op, table);
127
- let dbms = await this.cfg.db.getByIds(table, ids);
127
+ let dbms = await (opt.tx || this.cfg.db).getByIds(table, ids);
128
128
  if (!opt.raw && this.cfg.hooks.afterLoad && dbms.length) {
129
129
  dbms = (await (0, js_lib_1.pMap)(dbms, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(js_lib_1._isTruthy);
130
130
  }
@@ -662,7 +662,7 @@ class CommonDao {
662
662
  const started = this.logSaveStarted(op, bms, table);
663
663
  const { excludeFromIndexes } = this.cfg;
664
664
  const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds;
665
- await this.cfg.db.saveBatch(table, dbms, {
665
+ await (opt.tx || this.cfg.db).saveBatch(table, dbms, {
666
666
  excludeFromIndexes,
667
667
  assignGeneratedIds,
668
668
  ...opt,
@@ -781,7 +781,7 @@ class CommonDao {
781
781
  const op = `deleteByIds(${ids.join(', ')})`;
782
782
  const table = opt.table || this.cfg.table;
783
783
  const started = this.logStarted(op, table);
784
- const count = await this.cfg.db.deleteByIds(table, ids, opt);
784
+ const count = await (opt.tx || this.cfg.db).deleteByIds(table, ids, opt);
785
785
  this.logSaveResult(started, op, table);
786
786
  return count;
787
787
  }
@@ -991,21 +991,17 @@ class CommonDao {
991
991
  async ping() {
992
992
  await this.cfg.db.ping();
993
993
  }
994
- async useTransaction(fn) {
995
- const tx = await this.cfg.db.createTransaction();
996
- const daoTx = new CommonDaoTransaction(tx);
997
- try {
998
- await fn(daoTx);
999
- await daoTx.commit();
1000
- }
1001
- catch (err) {
1002
- await daoTx.rollback();
1003
- throw err;
1004
- }
1005
- }
1006
- async createTransaction() {
1007
- const tx = await this.cfg.db.createTransaction();
1008
- return new CommonDaoTransaction(tx);
994
+ async runInTransaction(fn) {
995
+ await this.cfg.db.runInTransaction(async (tx) => {
996
+ const daoTx = new CommonDaoTransaction(tx, this.cfg.logger);
997
+ try {
998
+ await fn(daoTx);
999
+ }
1000
+ catch (err) {
1001
+ await daoTx.rollback();
1002
+ throw err;
1003
+ }
1004
+ });
1009
1005
  }
1010
1006
  logResult(started, op, res, table) {
1011
1007
  if (!this.cfg.logLevel)
@@ -1062,65 +1058,61 @@ class CommonDao {
1062
1058
  }
1063
1059
  }
1064
1060
  exports.CommonDao = CommonDao;
1061
+ /**
1062
+ * Transaction context.
1063
+ * Has similar API than CommonDao, but all operations are performed in the context of the transaction.
1064
+ */
1065
1065
  class CommonDaoTransaction {
1066
- constructor(tx) {
1066
+ constructor(tx, logger) {
1067
1067
  this.tx = tx;
1068
+ this.logger = logger;
1068
1069
  }
1069
- async commit() {
1070
- await this.tx.commit();
1071
- }
1070
+ /**
1071
+ * Perform a graceful rollback without throwing/re-throwing any error.
1072
+ */
1072
1073
  async rollback() {
1073
1074
  try {
1074
1075
  await this.tx.rollback();
1075
1076
  }
1076
1077
  catch (err) {
1077
- console.log(err);
1078
+ // graceful rollback without re-throw
1079
+ this.logger.error(err);
1078
1080
  }
1079
1081
  }
1080
1082
  async getById(dao, id, opt) {
1083
+ if (!id)
1084
+ return null;
1081
1085
  return (await this.getByIds(dao, [id], opt))[0] || null;
1082
1086
  }
1083
1087
  async getByIds(dao, ids, opt) {
1084
- try {
1085
- return await dao.getByIds(ids, { ...opt, tx: this.tx });
1086
- }
1087
- catch (err) {
1088
- await this.rollback();
1089
- throw err;
1090
- }
1091
- }
1092
- async runQuery(dao, q, opt) {
1093
- try {
1094
- return await dao.runQuery(q, { ...opt, tx: this.tx });
1095
- }
1096
- catch (err) {
1097
- await this.rollback();
1098
- throw err;
1099
- }
1100
- }
1088
+ return await dao.getByIds(ids, { ...opt, tx: this.tx });
1089
+ }
1090
+ // todo: Queries inside Transaction are not supported yet
1091
+ // async runQuery<BM extends Partial<ObjectWithId>, DBM extends ObjectWithId>(
1092
+ // dao: CommonDao<BM, DBM, any>,
1093
+ // q: DBQuery<DBM>,
1094
+ // opt?: CommonDaoOptions,
1095
+ // ): Promise<Saved<BM>[]> {
1096
+ // try {
1097
+ // return await dao.runQuery(q, { ...opt, tx: this.tx })
1098
+ // } catch (err) {
1099
+ // await this.rollback()
1100
+ // throw err
1101
+ // }
1102
+ // }
1101
1103
  async save(dao, bm, opt) {
1102
1104
  return (await this.saveBatch(dao, [bm], opt))[0];
1103
1105
  }
1104
1106
  async saveBatch(dao, bms, opt) {
1105
- try {
1106
- return await dao.saveBatch(bms, { ...opt, tx: this.tx });
1107
- }
1108
- catch (err) {
1109
- await this.rollback();
1110
- throw err;
1111
- }
1107
+ return await dao.saveBatch(bms, { ...opt, tx: this.tx });
1112
1108
  }
1113
1109
  async deleteById(dao, id, opt) {
1110
+ if (!id)
1111
+ return 0;
1114
1112
  return await this.deleteByIds(dao, [id], opt);
1115
1113
  }
1116
1114
  async deleteByIds(dao, ids, opt) {
1117
- try {
1118
- return await dao.deleteByIds(ids, { ...opt, tx: this.tx });
1119
- }
1120
- catch (err) {
1121
- await this.rollback();
1122
- throw err;
1123
- }
1115
+ return await dao.deleteByIds(ids, { ...opt, tx: this.tx });
1124
1116
  }
1125
1117
  }
1126
1118
  exports.CommonDaoTransaction = CommonDaoTransaction;
@@ -1,4 +1,5 @@
1
1
  import type { ObjectWithId } from '@naturalcycles/js-lib';
2
+ import { CommonDB } from './common.db';
2
3
  /**
3
4
  * Similar to SQL INSERT, UPDATE.
4
5
  * Insert will fail if row already exists.
@@ -8,8 +9,24 @@ import type { ObjectWithId } from '@naturalcycles/js-lib';
8
9
  * Default is Upsert.
9
10
  */
10
11
  export type CommonDBSaveMethod = 'upsert' | 'insert' | 'update';
12
+ /**
13
+ * Transaction is committed when the function returns resolved Promise (aka "returns normally").
14
+ *
15
+ * Transaction is rolled back when the function returns rejected Promise (aka "throws").
16
+ */
17
+ export type DBTransactionFn = (tx: DBTransaction) => Promise<void>;
18
+ /**
19
+ * Transaction context.
20
+ * Has similar API than CommonDB, but all operations are performed in the context of the transaction.
21
+ */
11
22
  export interface DBTransaction {
12
- commit: () => Promise<void>;
23
+ getByIds: CommonDB['getByIds'];
24
+ saveBatch: CommonDB['saveBatch'];
25
+ deleteByIds: CommonDB['deleteByIds'];
26
+ /**
27
+ * Perform a graceful rollback.
28
+ * It'll rollback the transaction and won't throw/re-throw any errors.
29
+ */
13
30
  rollback: () => Promise<void>;
14
31
  }
15
32
  export interface CommonDBOptions {