@naturalcycles/db-lib 9.3.1 → 9.4.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.
@@ -60,11 +60,11 @@ export declare class CommonDao<BM extends PartialObjectWithId, DBM extends Parti
60
60
  runQueryExtendedAsTM(q: DBQuery<DBM>, opt?: CommonDaoOptions): Promise<RunQueryResult<TM>>;
61
61
  runQueryCount(q: DBQuery<DBM>, opt?: CommonDaoOptions): Promise<number>;
62
62
  streamQueryForEach(q: DBQuery<DBM>, mapper: AsyncMapper<Saved<BM>, void>, opt?: CommonDaoStreamForEachOptions<Saved<BM>>): Promise<void>;
63
- streamQueryAsDBMForEach(q: DBQuery<DBM>, mapper: AsyncMapper<DBM, void>, opt?: CommonDaoStreamForEachOptions<DBM>): Promise<void>;
63
+ streamQueryAsDBMForEach(q: DBQuery<DBM>, mapper: AsyncMapper<Saved<DBM>, void>, opt?: CommonDaoStreamForEachOptions<DBM>): Promise<void>;
64
64
  /**
65
65
  * Stream as Readable, to be able to .pipe() it further with support of backpressure.
66
66
  */
67
- streamQueryAsDBM(q: DBQuery<DBM>, opt?: CommonDaoStreamOptions<DBM>): ReadableTyped<DBM>;
67
+ streamQueryAsDBM(q: DBQuery<DBM>, opt?: CommonDaoStreamOptions<DBM>): ReadableTyped<Saved<DBM>>;
68
68
  /**
69
69
  * Stream as Readable, to be able to .pipe() it further with support of backpressure.
70
70
  *
@@ -106,6 +106,10 @@ export declare class CommonDao<BM extends PartialObjectWithId, DBM extends Parti
106
106
  * 3. Saves (as fast as possible since the read) with the Patch applied, but only if the data has changed.
107
107
  */
108
108
  patchById(id: string, patch: Partial<BM>, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>>;
109
+ /**
110
+ * Like patchById, but runs all operations within a Transaction.
111
+ */
112
+ patchByIdInTransaction(id: string, patch: Partial<BM>, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>>;
109
113
  /**
110
114
  * Same as patchById, but takes the whole object as input.
111
115
  * This "whole object" is mutated with the patch and returned.
@@ -113,6 +117,10 @@ export declare class CommonDao<BM extends PartialObjectWithId, DBM extends Parti
113
117
  * It still loads the row from the DB.
114
118
  */
115
119
  patch(bm: Saved<BM>, patch: Partial<BM>, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>>;
120
+ /**
121
+ * Like patch, but runs all operations within a Transaction.
122
+ */
123
+ patchInTransaction(bm: Saved<BM>, patch: Partial<BM>, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>>;
116
124
  saveAsDBM(dbm: DBM, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<DBM>>;
117
125
  saveBatch(bms: BM[], opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>[]>;
118
126
  saveBatchAsDBM(dbms: DBM[], opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<DBM>[]>;
@@ -160,14 +168,14 @@ export declare class CommonDao<BM extends PartialObjectWithId, DBM extends Parti
160
168
  *
161
169
  * Does NOT mutate the object.
162
170
  */
163
- validateAndConvert<IN, OUT = IN>(obj: Partial<IN>, schema: ObjectSchema<IN> | AjvSchema<IN> | ZodSchema<IN> | undefined, modelType: DBModelType, opt?: CommonDaoOptions): OUT;
171
+ validateAndConvert<T>(obj: Partial<T>, schema: ObjectSchema<T> | AjvSchema<T> | ZodSchema<T> | undefined, modelType: DBModelType, opt?: CommonDaoOptions): any;
164
172
  getTableSchema(): Promise<JsonSchemaRootObject<DBM>>;
165
173
  createTable(schema: JsonSchemaObject<DBM>, opt?: CommonDaoCreateOptions): Promise<void>;
166
174
  /**
167
175
  * Proxy to this.cfg.db.ping
168
176
  */
169
177
  ping(): Promise<void>;
170
- runInTransaction(fn: CommonDaoTransactionFn, opt?: CommonDBTransactionOptions): Promise<void>;
178
+ runInTransaction<T = void>(fn: CommonDaoTransactionFn<T>, opt?: CommonDBTransactionOptions): Promise<T>;
171
179
  protected logResult(started: number, op: string, res: any, table: string): void;
172
180
  protected logSaveResult(started: number, op: string, table: string): void;
173
181
  protected logStarted(op: string, table: string, force?: boolean): UnixTimestampMillisNumber;
@@ -178,13 +186,13 @@ export declare class CommonDao<BM extends PartialObjectWithId, DBM extends Parti
178
186
  *
179
187
  * Transaction is rolled back when the function returns rejected Promise (aka "throws").
180
188
  */
181
- export type CommonDaoTransactionFn = (tx: CommonDaoTransaction) => Promise<void>;
189
+ export type CommonDaoTransactionFn<T = void> = (tx: CommonDaoTransaction) => Promise<T>;
182
190
  /**
183
191
  * Transaction context.
184
192
  * Has similar API than CommonDao, but all operations are performed in the context of the transaction.
185
193
  */
186
194
  export declare class CommonDaoTransaction {
187
- private tx;
195
+ tx: DBTransaction;
188
196
  private logger;
189
197
  constructor(tx: DBTransaction, logger: CommonLogger);
190
198
  /**
@@ -24,7 +24,7 @@ class CommonDao {
24
24
  // otherwise to log Operations
25
25
  // e.g in Dev (local machine), Test - it will log operations (useful for debugging)
26
26
  logLevel: isGAE || isCI ? common_dao_model_1.CommonDaoLogLevel.NONE : common_dao_model_1.CommonDaoLogLevel.OPERATIONS,
27
- createId: true,
27
+ generateId: true,
28
28
  assignGeneratedIds: false,
29
29
  useCreatedProperty: true,
30
30
  useUpdatedProperty: true,
@@ -42,7 +42,7 @@ class CommonDao {
42
42
  ...cfg.hooks,
43
43
  },
44
44
  };
45
- if (this.cfg.createId) {
45
+ if (this.cfg.generateId) {
46
46
  this.cfg.hooks.createRandomId ||= () => (0, nodejs_lib_1.stringId)();
47
47
  }
48
48
  else {
@@ -62,7 +62,7 @@ class CommonDao {
62
62
  const op = `getById(${id})`;
63
63
  const table = opt.table || this.cfg.table;
64
64
  const started = this.logStarted(op, table);
65
- let dbm = (await this.cfg.db.getByIds(table, [id]))[0];
65
+ let dbm = (await (opt.tx || this.cfg.db).getByIds(table, [id]))[0];
66
66
  if (dbm && !opt.raw && this.cfg.hooks.afterLoad) {
67
67
  dbm = (await this.cfg.hooks.afterLoad(dbm)) || undefined;
68
68
  }
@@ -89,7 +89,7 @@ class CommonDao {
89
89
  const op = `getByIdAsDBM(${id})`;
90
90
  const table = opt.table || this.cfg.table;
91
91
  const started = this.logStarted(op, table);
92
- let [dbm] = await this.cfg.db.getByIds(table, [id]);
92
+ let [dbm] = await (opt.tx || this.cfg.db).getByIds(table, [id]);
93
93
  if (dbm && !opt.raw && this.cfg.hooks.afterLoad) {
94
94
  dbm = (await this.cfg.hooks.afterLoad(dbm)) || undefined;
95
95
  }
@@ -105,7 +105,7 @@ class CommonDao {
105
105
  const op = `getByIdAsTM(${id})`;
106
106
  const table = opt.table || this.cfg.table;
107
107
  const started = this.logStarted(op, table);
108
- let [dbm] = await this.cfg.db.getByIds(table, [id]);
108
+ let [dbm] = await (opt.tx || this.cfg.db).getByIds(table, [id]);
109
109
  if (dbm && !opt.raw && this.cfg.hooks.afterLoad) {
110
110
  dbm = (await this.cfg.hooks.afterLoad(dbm)) || undefined;
111
111
  }
@@ -138,7 +138,7 @@ class CommonDao {
138
138
  const op = `getByIdsAsDBM ${ids.length} id(s) (${(0, js_lib_1._truncate)(ids.slice(0, 10).join(', '), 50)})`;
139
139
  const table = opt.table || this.cfg.table;
140
140
  const started = this.logStarted(op, table);
141
- let dbms = await this.cfg.db.getByIds(table, ids);
141
+ let dbms = await (opt.tx || this.cfg.db).getByIds(table, ids);
142
142
  if (!opt.raw && this.cfg.hooks.afterLoad && dbms.length) {
143
143
  dbms = (await (0, js_lib_1.pMap)(dbms, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(js_lib_1._isTruthy);
144
144
  }
@@ -491,7 +491,7 @@ class CommonDao {
491
491
  if (this.cfg.useUpdatedProperty) {
492
492
  obj.updated = opt.preserveUpdatedCreated && obj.updated ? obj.updated : now;
493
493
  }
494
- if (this.cfg.createId) {
494
+ if (this.cfg.generateId) {
495
495
  obj.id ||= this.cfg.hooks.createNaturalId?.(obj) || this.cfg.hooks.createRandomId();
496
496
  }
497
497
  return obj;
@@ -506,7 +506,7 @@ class CommonDao {
506
506
  // Skipping the save operation
507
507
  return bm;
508
508
  }
509
- const idWasGenerated = !bm.id && this.cfg.createId;
509
+ const idWasGenerated = !bm.id && this.cfg.generateId;
510
510
  this.assignIdCreatedUpdated(bm, opt); // mutates
511
511
  let dbm = await this.bmToDBM(bm, opt);
512
512
  if (this.cfg.hooks.beforeSave) {
@@ -524,7 +524,7 @@ class CommonDao {
524
524
  const started = this.logSaveStarted(op, bm, table);
525
525
  const { excludeFromIndexes } = this.cfg;
526
526
  const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds;
527
- await this.cfg.db.saveBatch(table, [dbm], {
527
+ await (opt.tx || this.cfg.db).saveBatch(table, [dbm], {
528
528
  excludeFromIndexes,
529
529
  assignGeneratedIds,
530
530
  ...opt,
@@ -566,6 +566,12 @@ class CommonDao {
566
566
  * 3. Saves (as fast as possible since the read) with the Patch applied, but only if the data has changed.
567
567
  */
568
568
  async patchById(id, patch, opt = {}) {
569
+ if (this.cfg.patchInTransaction && !opt.tx) {
570
+ // patchInTransaction means that we should run this op in Transaction
571
+ // But if opt.tx is passed - means that we are already in a Transaction,
572
+ // and should just continue as-is
573
+ return await this.patchByIdInTransaction(id, patch, opt);
574
+ }
569
575
  let patched;
570
576
  const loaded = await this.getById(id, opt);
571
577
  if (loaded) {
@@ -580,6 +586,14 @@ class CommonDao {
580
586
  }
581
587
  return await this.save(patched, opt);
582
588
  }
589
+ /**
590
+ * Like patchById, but runs all operations within a Transaction.
591
+ */
592
+ async patchByIdInTransaction(id, patch, opt) {
593
+ return await this.runInTransaction(async (daoTx) => {
594
+ return await this.patchById(id, patch, { ...opt, tx: daoTx.tx });
595
+ });
596
+ }
583
597
  /**
584
598
  * Same as patchById, but takes the whole object as input.
585
599
  * This "whole object" is mutated with the patch and returned.
@@ -587,9 +601,12 @@ class CommonDao {
587
601
  * It still loads the row from the DB.
588
602
  */
589
603
  async patch(bm, patch, opt = {}) {
590
- (0, js_lib_1._assert)(bm.id, 'patch argument object should have an id', {
591
- bm,
592
- });
604
+ if (this.cfg.patchInTransaction && !opt.tx) {
605
+ // patchInTransaction means that we should run this op in Transaction
606
+ // But if opt.tx is passed - means that we are already in a Transaction,
607
+ // and should just continue as-is
608
+ return await this.patchInTransaction(bm, patch, opt);
609
+ }
593
610
  const loaded = await this.getById(bm.id, opt);
594
611
  if (loaded) {
595
612
  Object.assign(loaded, patch);
@@ -605,6 +622,14 @@ class CommonDao {
605
622
  }
606
623
  return await this.save(bm, opt);
607
624
  }
625
+ /**
626
+ * Like patch, but runs all operations within a Transaction.
627
+ */
628
+ async patchInTransaction(bm, patch, opt) {
629
+ return await this.runInTransaction(async (daoTx) => {
630
+ return await this.patch(bm, patch, { ...opt, tx: daoTx.tx });
631
+ });
632
+ }
608
633
  async saveAsDBM(dbm, opt = {}) {
609
634
  this.requireWriteAccess();
610
635
  const table = opt.table || this.cfg.table;
@@ -612,7 +637,7 @@ class CommonDao {
612
637
  // will override/set `updated` field, unless opts.preserveUpdated is set
613
638
  let row = dbm;
614
639
  if (!opt.raw) {
615
- const idWasGenerated = !dbm.id && this.cfg.createId;
640
+ const idWasGenerated = !dbm.id && this.cfg.generateId;
616
641
  this.assignIdCreatedUpdated(dbm, opt); // mutates
617
642
  row = this.anyToDBM(dbm, opt);
618
643
  if (opt.ensureUniqueId && idWasGenerated)
@@ -630,7 +655,7 @@ class CommonDao {
630
655
  if (row === null)
631
656
  return dbm;
632
657
  }
633
- await this.cfg.db.saveBatch(table, [row], {
658
+ await (opt.tx || this.cfg.db).saveBatch(table, [row], {
634
659
  excludeFromIndexes,
635
660
  assignGeneratedIds,
636
661
  ...opt,
@@ -699,7 +724,7 @@ class CommonDao {
699
724
  if (this.cfg.hooks.beforeSave && rows.length) {
700
725
  rows = (await (0, js_lib_1.pMap)(rows, async (row) => await this.cfg.hooks.beforeSave(row))).filter(js_lib_1._isTruthy);
701
726
  }
702
- await this.cfg.db.saveBatch(table, rows, {
727
+ await (opt.tx || this.cfg.db).saveBatch(table, rows, {
703
728
  excludeFromIndexes,
704
729
  assignGeneratedIds,
705
730
  ...opt,
@@ -858,7 +883,6 @@ class CommonDao {
858
883
  // DBM > BM
859
884
  const bm = await this.cfg.hooks.beforeDBMToBM(dbm);
860
885
  // Validate/convert BM
861
- // eslint-disable-next-line @typescript-eslint/return-await
862
886
  return this.validateAndConvert(bm, this.cfg.bmSchema, db_model_1.DBModelType.BM, opt);
863
887
  }
864
888
  async dbmsToBM(dbms, opt = {}) {
@@ -876,7 +900,6 @@ class CommonDao {
876
900
  // BM > DBM
877
901
  const dbm = { ...(await this.cfg.hooks.beforeBMToDBM(bm)) };
878
902
  // Validate/convert DBM
879
- // eslint-disable-next-line @typescript-eslint/return-await
880
903
  return this.validateAndConvert(dbm, this.cfg.dbmSchema, db_model_1.DBModelType.DBM, opt);
881
904
  }
882
905
  async bmsToDBM(bms, opt = {}) {
@@ -993,16 +1016,18 @@ class CommonDao {
993
1016
  await this.cfg.db.ping();
994
1017
  }
995
1018
  async runInTransaction(fn, opt) {
1019
+ let r;
996
1020
  await this.cfg.db.runInTransaction(async (tx) => {
997
1021
  const daoTx = new CommonDaoTransaction(tx, this.cfg.logger);
998
1022
  try {
999
- await fn(daoTx);
1023
+ r = await fn(daoTx);
1000
1024
  }
1001
1025
  catch (err) {
1002
1026
  await daoTx.rollback(); // graceful rollback that "never throws"
1003
1027
  throw err;
1004
1028
  }
1005
1029
  }, opt);
1030
+ return r;
1006
1031
  }
1007
1032
  logResult(started, op, res, table) {
1008
1033
  if (!this.cfg.logLevel)
@@ -1081,9 +1106,7 @@ class CommonDaoTransaction {
1081
1106
  }
1082
1107
  }
1083
1108
  async getById(dao, id, opt) {
1084
- if (!id)
1085
- return null;
1086
- return (await this.getByIds(dao, [id], opt))[0] || null;
1109
+ return await dao.getById(id, { ...opt, tx: this.tx });
1087
1110
  }
1088
1111
  async getByIds(dao, ids, opt) {
1089
1112
  return await dao.getByIds(ids, { ...opt, tx: this.tx });
@@ -147,7 +147,7 @@ export interface CommonDaoCfg<BM extends PartialObjectWithId, DBM extends Partia
147
147
  * Set to false to disable auto-generation of `id`.
148
148
  * Useful e.g when your DB is generating ids by itself (e.g mysql auto_increment).
149
149
  */
150
- createId?: boolean;
150
+ generateId?: boolean;
151
151
  /**
152
152
  * See the same option in CommonDB.
153
153
  * Defaults to false normally.
@@ -173,6 +173,13 @@ export interface CommonDaoCfg<BM extends PartialObjectWithId, DBM extends Partia
173
173
  * @deprecated
174
174
  */
175
175
  filterNullishValues?: boolean;
176
+ /**
177
+ * Defaults to false.
178
+ * If true - run patch operations (patch, patchById) in a Transaction.
179
+ *
180
+ * @experimental
181
+ */
182
+ patchInTransaction?: boolean;
176
183
  }
177
184
  /**
178
185
  * All properties default to undefined.
@@ -100,9 +100,9 @@ export declare class RunnableDBQuery<BM extends PartialObjectWithId, DBM extends
100
100
  runQueryCount(opt?: CommonDaoOptions): Promise<number>;
101
101
  updateByQuery(patch: DBPatch<DBM>, opt?: CommonDaoOptions): Promise<number>;
102
102
  streamQueryForEach(mapper: AsyncMapper<Saved<BM>, void>, opt?: CommonDaoStreamForEachOptions<Saved<BM>>): Promise<void>;
103
- streamQueryAsDBMForEach(mapper: AsyncMapper<DBM, void>, opt?: CommonDaoStreamForEachOptions<DBM>): Promise<void>;
103
+ streamQueryAsDBMForEach(mapper: AsyncMapper<Saved<DBM>, void>, opt?: CommonDaoStreamForEachOptions<DBM>): Promise<void>;
104
104
  streamQuery(opt?: CommonDaoStreamOptions<Saved<BM>>): ReadableTyped<Saved<BM>>;
105
- streamQueryAsDBM(opt?: CommonDaoStreamOptions<DBM>): ReadableTyped<DBM>;
105
+ streamQueryAsDBM(opt?: CommonDaoStreamOptions<DBM>): ReadableTyped<Saved<DBM>>;
106
106
  queryIds(opt?: CommonDaoOptions): Promise<string[]>;
107
107
  streamQueryIds(opt?: CommonDaoStreamOptions<string>): ReadableTyped<string>;
108
108
  streamQueryIdsForEach(mapper: AsyncMapper<string, void>, opt?: CommonDaoStreamForEachOptions<string>): Promise<void>;
package/package.json CHANGED
@@ -40,7 +40,7 @@
40
40
  "engines": {
41
41
  "node": ">=18.12"
42
42
  },
43
- "version": "9.3.1",
43
+ "version": "9.4.0",
44
44
  "description": "Lowest Common Denominator API to supported Databases",
45
45
  "keywords": [
46
46
  "db",
@@ -193,7 +193,7 @@ export interface CommonDaoCfg<
193
193
  * Set to false to disable auto-generation of `id`.
194
194
  * Useful e.g when your DB is generating ids by itself (e.g mysql auto_increment).
195
195
  */
196
- createId?: boolean
196
+ generateId?: boolean
197
197
 
198
198
  /**
199
199
  * See the same option in CommonDB.
@@ -223,6 +223,14 @@ export interface CommonDaoCfg<
223
223
  * @deprecated
224
224
  */
225
225
  filterNullishValues?: boolean
226
+
227
+ /**
228
+ * Defaults to false.
229
+ * If true - run patch operations (patch, patchById) in a Transaction.
230
+ *
231
+ * @experimental
232
+ */
233
+ patchInTransaction?: boolean
226
234
  }
227
235
 
228
236
  /**
@@ -87,7 +87,7 @@ export class CommonDao<
87
87
  // otherwise to log Operations
88
88
  // e.g in Dev (local machine), Test - it will log operations (useful for debugging)
89
89
  logLevel: isGAE || isCI ? CommonDaoLogLevel.NONE : CommonDaoLogLevel.OPERATIONS,
90
- createId: true,
90
+ generateId: true,
91
91
  assignGeneratedIds: false,
92
92
  useCreatedProperty: true,
93
93
  useUpdatedProperty: true,
@@ -106,7 +106,7 @@ export class CommonDao<
106
106
  } satisfies Partial<CommonDaoHooks<BM, DBM, TM>>,
107
107
  }
108
108
 
109
- if (this.cfg.createId) {
109
+ if (this.cfg.generateId) {
110
110
  this.cfg.hooks!.createRandomId ||= () => stringId()
111
111
  } else {
112
112
  delete this.cfg.hooks!.createRandomId
@@ -130,7 +130,7 @@ export class CommonDao<
130
130
  const table = opt.table || this.cfg.table
131
131
  const started = this.logStarted(op, table)
132
132
 
133
- let dbm = (await this.cfg.db.getByIds<DBM>(table, [id]))[0]
133
+ let dbm = (await (opt.tx || this.cfg.db).getByIds<DBM>(table, [id]))[0]
134
134
  if (dbm && !opt.raw && this.cfg.hooks!.afterLoad) {
135
135
  dbm = (await this.cfg.hooks!.afterLoad(dbm)) || undefined
136
136
  }
@@ -170,7 +170,7 @@ export class CommonDao<
170
170
  const op = `getByIdAsDBM(${id})`
171
171
  const table = opt.table || this.cfg.table
172
172
  const started = this.logStarted(op, table)
173
- let [dbm] = await this.cfg.db.getByIds<DBM>(table, [id])
173
+ let [dbm] = await (opt.tx || this.cfg.db).getByIds<DBM>(table, [id])
174
174
  if (dbm && !opt.raw && this.cfg.hooks!.afterLoad) {
175
175
  dbm = (await this.cfg.hooks!.afterLoad(dbm)) || undefined
176
176
  }
@@ -189,7 +189,7 @@ export class CommonDao<
189
189
  const op = `getByIdAsTM(${id})`
190
190
  const table = opt.table || this.cfg.table
191
191
  const started = this.logStarted(op, table)
192
- let [dbm] = await this.cfg.db.getByIds<DBM>(table, [id])
192
+ let [dbm] = await (opt.tx || this.cfg.db).getByIds<DBM>(table, [id])
193
193
  if (dbm && !opt.raw && this.cfg.hooks!.afterLoad) {
194
194
  dbm = (await this.cfg.hooks!.afterLoad(dbm)) || undefined
195
195
  }
@@ -226,7 +226,7 @@ export class CommonDao<
226
226
  const op = `getByIdsAsDBM ${ids.length} id(s) (${_truncate(ids.slice(0, 10).join(', '), 50)})`
227
227
  const table = opt.table || this.cfg.table
228
228
  const started = this.logStarted(op, table)
229
- let dbms = await this.cfg.db.getByIds<DBM>(table, ids)
229
+ let dbms = await (opt.tx || this.cfg.db).getByIds<DBM>(table, ids)
230
230
  if (!opt.raw && this.cfg.hooks!.afterLoad && dbms.length) {
231
231
  dbms = (await pMap(dbms, async dbm => await this.cfg.hooks!.afterLoad!(dbm))).filter(
232
232
  _isTruthy,
@@ -484,7 +484,7 @@ export class CommonDao<
484
484
 
485
485
  async streamQueryAsDBMForEach(
486
486
  q: DBQuery<DBM>,
487
- mapper: AsyncMapper<DBM, void>,
487
+ mapper: AsyncMapper<Saved<DBM>, void>,
488
488
  opt: CommonDaoStreamForEachOptions<DBM> = {},
489
489
  ): Promise<void> {
490
490
  q.table = opt.table || q.table
@@ -515,7 +515,7 @@ export class CommonDao<
515
515
  errorMode: opt.errorMode,
516
516
  },
517
517
  ),
518
- transformMap<DBM, void>(mapper, {
518
+ transformMap<Saved<DBM>, void>(mapper, {
519
519
  ...opt,
520
520
  predicate: _passthroughPredicate, // to be able to logProgress
521
521
  }),
@@ -535,7 +535,10 @@ export class CommonDao<
535
535
  /**
536
536
  * Stream as Readable, to be able to .pipe() it further with support of backpressure.
537
537
  */
538
- streamQueryAsDBM(q: DBQuery<DBM>, opt: CommonDaoStreamOptions<DBM> = {}): ReadableTyped<DBM> {
538
+ streamQueryAsDBM(
539
+ q: DBQuery<DBM>,
540
+ opt: CommonDaoStreamOptions<DBM> = {},
541
+ ): ReadableTyped<Saved<DBM>> {
539
542
  q.table = opt.table || q.table
540
543
  opt.skipValidation = opt.skipValidation !== false // default true
541
544
  opt.skipConversion = opt.skipConversion !== false // default true
@@ -686,7 +689,7 @@ export class CommonDao<
686
689
  obj.updated = opt.preserveUpdatedCreated && obj.updated ? obj.updated : now
687
690
  }
688
691
 
689
- if (this.cfg.createId) {
692
+ if (this.cfg.generateId) {
690
693
  obj.id ||= this.cfg.hooks!.createNaturalId?.(obj as any) || this.cfg.hooks!.createRandomId!()
691
694
  }
692
695
 
@@ -705,7 +708,7 @@ export class CommonDao<
705
708
  return bm as Saved<BM>
706
709
  }
707
710
 
708
- const idWasGenerated = !bm.id && this.cfg.createId
711
+ const idWasGenerated = !bm.id && this.cfg.generateId
709
712
  this.assignIdCreatedUpdated(bm, opt) // mutates
710
713
  let dbm = await this.bmToDBM(bm, opt)
711
714
 
@@ -724,7 +727,7 @@ export class CommonDao<
724
727
  const { excludeFromIndexes } = this.cfg
725
728
  const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds
726
729
 
727
- await this.cfg.db.saveBatch(table, [dbm], {
730
+ await (opt.tx || this.cfg.db).saveBatch(table, [dbm], {
728
731
  excludeFromIndexes,
729
732
  assignGeneratedIds,
730
733
  ...opt,
@@ -781,6 +784,13 @@ export class CommonDao<
781
784
  patch: Partial<BM>,
782
785
  opt: CommonDaoSaveBatchOptions<DBM> = {},
783
786
  ): Promise<Saved<BM>> {
787
+ if (this.cfg.patchInTransaction && !opt.tx) {
788
+ // patchInTransaction means that we should run this op in Transaction
789
+ // But if opt.tx is passed - means that we are already in a Transaction,
790
+ // and should just continue as-is
791
+ return await this.patchByIdInTransaction(id, patch, opt)
792
+ }
793
+
784
794
  let patched: Saved<BM>
785
795
  const loaded = await this.getById(id, opt)
786
796
 
@@ -798,6 +808,19 @@ export class CommonDao<
798
808
  return await this.save(patched, opt)
799
809
  }
800
810
 
811
+ /**
812
+ * Like patchById, but runs all operations within a Transaction.
813
+ */
814
+ async patchByIdInTransaction(
815
+ id: string,
816
+ patch: Partial<BM>,
817
+ opt?: CommonDaoSaveBatchOptions<DBM>,
818
+ ): Promise<Saved<BM>> {
819
+ return await this.runInTransaction(async daoTx => {
820
+ return await this.patchById(id, patch, { ...opt, tx: daoTx.tx })
821
+ })
822
+ }
823
+
801
824
  /**
802
825
  * Same as patchById, but takes the whole object as input.
803
826
  * This "whole object" is mutated with the patch and returned.
@@ -809,9 +832,12 @@ export class CommonDao<
809
832
  patch: Partial<BM>,
810
833
  opt: CommonDaoSaveBatchOptions<DBM> = {},
811
834
  ): Promise<Saved<BM>> {
812
- _assert(bm.id, 'patch argument object should have an id', {
813
- bm,
814
- })
835
+ if (this.cfg.patchInTransaction && !opt.tx) {
836
+ // patchInTransaction means that we should run this op in Transaction
837
+ // But if opt.tx is passed - means that we are already in a Transaction,
838
+ // and should just continue as-is
839
+ return await this.patchInTransaction(bm, patch, opt)
840
+ }
815
841
 
816
842
  const loaded = await this.getById(bm.id, opt)
817
843
 
@@ -832,6 +858,19 @@ export class CommonDao<
832
858
  return await this.save(bm, opt)
833
859
  }
834
860
 
861
+ /**
862
+ * Like patch, but runs all operations within a Transaction.
863
+ */
864
+ async patchInTransaction(
865
+ bm: Saved<BM>,
866
+ patch: Partial<BM>,
867
+ opt?: CommonDaoSaveBatchOptions<DBM>,
868
+ ): Promise<Saved<BM>> {
869
+ return await this.runInTransaction(async daoTx => {
870
+ return await this.patch(bm, patch, { ...opt, tx: daoTx.tx })
871
+ })
872
+ }
873
+
835
874
  async saveAsDBM(dbm: DBM, opt: CommonDaoSaveBatchOptions<DBM> = {}): Promise<Saved<DBM>> {
836
875
  this.requireWriteAccess()
837
876
  const table = opt.table || this.cfg.table
@@ -840,7 +879,7 @@ export class CommonDao<
840
879
  // will override/set `updated` field, unless opts.preserveUpdated is set
841
880
  let row = dbm as Saved<DBM>
842
881
  if (!opt.raw) {
843
- const idWasGenerated = !dbm.id && this.cfg.createId
882
+ const idWasGenerated = !dbm.id && this.cfg.generateId
844
883
  this.assignIdCreatedUpdated(dbm, opt) // mutates
845
884
  row = this.anyToDBM(dbm, opt)
846
885
  if (opt.ensureUniqueId && idWasGenerated) await this.ensureUniqueId(table, row)
@@ -858,7 +897,7 @@ export class CommonDao<
858
897
  if (row === null) return dbm as Saved<DBM>
859
898
  }
860
899
 
861
- await this.cfg.db.saveBatch(table, [row], {
900
+ await (opt.tx || this.cfg.db).saveBatch(table, [row], {
862
901
  excludeFromIndexes,
863
902
  assignGeneratedIds,
864
903
  ...opt,
@@ -949,7 +988,7 @@ export class CommonDao<
949
988
  )
950
989
  }
951
990
 
952
- await this.cfg.db.saveBatch(table, rows, {
991
+ await (opt.tx || this.cfg.db).saveBatch(table, rows, {
953
992
  excludeFromIndexes,
954
993
  assignGeneratedIds,
955
994
  ...opt,
@@ -1160,7 +1199,7 @@ export class CommonDao<
1160
1199
  const bm = await this.cfg.hooks!.beforeDBMToBM!(dbm)
1161
1200
 
1162
1201
  // Validate/convert BM
1163
- // eslint-disable-next-line @typescript-eslint/return-await
1202
+
1164
1203
  return this.validateAndConvert(bm, this.cfg.bmSchema, DBModelType.BM, opt)
1165
1204
  }
1166
1205
 
@@ -1189,7 +1228,7 @@ export class CommonDao<
1189
1228
  const dbm = { ...(await this.cfg.hooks!.beforeBMToDBM!(bm)) }
1190
1229
 
1191
1230
  // Validate/convert DBM
1192
- // eslint-disable-next-line @typescript-eslint/return-await
1231
+
1193
1232
  return this.validateAndConvert(dbm, this.cfg.dbmSchema, DBModelType.DBM, opt)
1194
1233
  }
1195
1234
 
@@ -1248,14 +1287,14 @@ export class CommonDao<
1248
1287
  *
1249
1288
  * Does NOT mutate the object.
1250
1289
  */
1251
- validateAndConvert<IN, OUT = IN>(
1252
- obj: Partial<IN>,
1253
- schema: ObjectSchema<IN> | AjvSchema<IN> | ZodSchema<IN> | undefined,
1290
+ validateAndConvert<T>(
1291
+ obj: Partial<T>,
1292
+ schema: ObjectSchema<T> | AjvSchema<T> | ZodSchema<T> | undefined,
1254
1293
  modelType: DBModelType,
1255
1294
  opt: CommonDaoOptions = {},
1256
- ): OUT {
1295
+ ): any {
1257
1296
  // `raw` option completely bypasses any processing
1258
- if (opt.raw) return obj as any as OUT
1297
+ if (opt.raw) return obj as any
1259
1298
 
1260
1299
  // Kirill 2021-10-18: I realized that there's little reason to keep removing null values
1261
1300
  // So, from now on we'll preserve them
@@ -1274,31 +1313,31 @@ export class CommonDao<
1274
1313
 
1275
1314
  // Pre-validation hooks
1276
1315
  if (modelType === DBModelType.DBM) {
1277
- obj = this.cfg.hooks!.beforeDBMValidate!(obj as any) as IN
1316
+ obj = this.cfg.hooks!.beforeDBMValidate!(obj as any) as T
1278
1317
  }
1279
1318
 
1280
1319
  // Return as is if no schema is passed or if `skipConversion` is set
1281
1320
  if (!schema || opt.skipConversion) {
1282
- return obj as OUT
1321
+ return obj
1283
1322
  }
1284
1323
 
1285
1324
  // This will Convert and Validate
1286
1325
  const table = opt.table || this.cfg.table
1287
1326
  const objectName = table + (modelType || '')
1288
1327
 
1289
- let error: JoiValidationError | AjvValidationError | ZodValidationError<IN> | undefined
1328
+ let error: JoiValidationError | AjvValidationError | ZodValidationError<T> | undefined
1290
1329
  let convertedValue: any
1291
1330
 
1292
1331
  if (schema instanceof ZodSchema) {
1293
1332
  // Zod schema
1294
- const vr = zSafeValidate(obj as IN, schema)
1333
+ const vr = zSafeValidate(obj as T, schema)
1295
1334
  error = vr.error
1296
1335
  convertedValue = vr.data
1297
1336
  } else if (schema instanceof AjvSchema) {
1298
1337
  // Ajv schema
1299
1338
  convertedValue = obj // because Ajv mutates original object
1300
1339
 
1301
- error = schema.getValidationError(obj as IN, {
1340
+ error = schema.getValidationError(obj as T, {
1302
1341
  objectName,
1303
1342
  })
1304
1343
  } else {
@@ -1334,20 +1373,24 @@ export class CommonDao<
1334
1373
  await this.cfg.db.ping()
1335
1374
  }
1336
1375
 
1337
- async runInTransaction(
1338
- fn: CommonDaoTransactionFn,
1376
+ async runInTransaction<T = void>(
1377
+ fn: CommonDaoTransactionFn<T>,
1339
1378
  opt?: CommonDBTransactionOptions,
1340
- ): Promise<void> {
1379
+ ): Promise<T> {
1380
+ let r: T
1381
+
1341
1382
  await this.cfg.db.runInTransaction(async tx => {
1342
1383
  const daoTx = new CommonDaoTransaction(tx, this.cfg.logger!)
1343
1384
 
1344
1385
  try {
1345
- await fn(daoTx)
1386
+ r = await fn(daoTx)
1346
1387
  } catch (err) {
1347
1388
  await daoTx.rollback() // graceful rollback that "never throws"
1348
1389
  throw err
1349
1390
  }
1350
1391
  }, opt)
1392
+
1393
+ return r!
1351
1394
  }
1352
1395
 
1353
1396
  protected logResult(started: number, op: string, res: any, table: string): void {
@@ -1412,7 +1455,7 @@ export class CommonDao<
1412
1455
  *
1413
1456
  * Transaction is rolled back when the function returns rejected Promise (aka "throws").
1414
1457
  */
1415
- export type CommonDaoTransactionFn = (tx: CommonDaoTransaction) => Promise<void>
1458
+ export type CommonDaoTransactionFn<T = void> = (tx: CommonDaoTransaction) => Promise<T>
1416
1459
 
1417
1460
  /**
1418
1461
  * Transaction context.
@@ -1420,7 +1463,7 @@ export type CommonDaoTransactionFn = (tx: CommonDaoTransaction) => Promise<void>
1420
1463
  */
1421
1464
  export class CommonDaoTransaction {
1422
1465
  constructor(
1423
- private tx: DBTransaction,
1466
+ public tx: DBTransaction,
1424
1467
  private logger: CommonLogger,
1425
1468
  ) {}
1426
1469
 
@@ -1441,8 +1484,7 @@ export class CommonDaoTransaction {
1441
1484
  id?: string | null,
1442
1485
  opt?: CommonDaoOptions,
1443
1486
  ): Promise<Saved<BM> | null> {
1444
- if (!id) return null
1445
- return (await this.getByIds(dao, [id], opt))[0] || null
1487
+ return await dao.getById(id, { ...opt, tx: this.tx })
1446
1488
  }
1447
1489
 
1448
1490
  async getByIds<BM extends PartialObjectWithId, DBM extends PartialObjectWithId>(
@@ -297,7 +297,7 @@ export class RunnableDBQuery<
297
297
  }
298
298
 
299
299
  async streamQueryAsDBMForEach(
300
- mapper: AsyncMapper<DBM, void>,
300
+ mapper: AsyncMapper<Saved<DBM>, void>,
301
301
  opt?: CommonDaoStreamForEachOptions<DBM>,
302
302
  ): Promise<void> {
303
303
  await this.dao.streamQueryAsDBMForEach(this, mapper, opt)
@@ -307,7 +307,7 @@ export class RunnableDBQuery<
307
307
  return this.dao.streamQuery(this, opt)
308
308
  }
309
309
 
310
- streamQueryAsDBM(opt?: CommonDaoStreamOptions<DBM>): ReadableTyped<DBM> {
310
+ streamQueryAsDBM(opt?: CommonDaoStreamOptions<DBM>): ReadableTyped<Saved<DBM>> {
311
311
  return this.dao.streamQueryAsDBM(this, opt)
312
312
  }
313
313