@naturalcycles/db-lib 9.3.2 → 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.
@@ -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.
package/package.json CHANGED
@@ -40,7 +40,7 @@
40
40
  "engines": {
41
41
  "node": ">=18.12"
42
42
  },
43
- "version": "9.3.2",
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,
@@ -689,7 +689,7 @@ export class CommonDao<
689
689
  obj.updated = opt.preserveUpdatedCreated && obj.updated ? obj.updated : now
690
690
  }
691
691
 
692
- if (this.cfg.createId) {
692
+ if (this.cfg.generateId) {
693
693
  obj.id ||= this.cfg.hooks!.createNaturalId?.(obj as any) || this.cfg.hooks!.createRandomId!()
694
694
  }
695
695
 
@@ -708,7 +708,7 @@ export class CommonDao<
708
708
  return bm as Saved<BM>
709
709
  }
710
710
 
711
- const idWasGenerated = !bm.id && this.cfg.createId
711
+ const idWasGenerated = !bm.id && this.cfg.generateId
712
712
  this.assignIdCreatedUpdated(bm, opt) // mutates
713
713
  let dbm = await this.bmToDBM(bm, opt)
714
714
 
@@ -727,7 +727,7 @@ export class CommonDao<
727
727
  const { excludeFromIndexes } = this.cfg
728
728
  const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds
729
729
 
730
- await this.cfg.db.saveBatch(table, [dbm], {
730
+ await (opt.tx || this.cfg.db).saveBatch(table, [dbm], {
731
731
  excludeFromIndexes,
732
732
  assignGeneratedIds,
733
733
  ...opt,
@@ -784,6 +784,13 @@ export class CommonDao<
784
784
  patch: Partial<BM>,
785
785
  opt: CommonDaoSaveBatchOptions<DBM> = {},
786
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
+
787
794
  let patched: Saved<BM>
788
795
  const loaded = await this.getById(id, opt)
789
796
 
@@ -801,6 +808,19 @@ export class CommonDao<
801
808
  return await this.save(patched, opt)
802
809
  }
803
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
+
804
824
  /**
805
825
  * Same as patchById, but takes the whole object as input.
806
826
  * This "whole object" is mutated with the patch and returned.
@@ -812,9 +832,12 @@ export class CommonDao<
812
832
  patch: Partial<BM>,
813
833
  opt: CommonDaoSaveBatchOptions<DBM> = {},
814
834
  ): Promise<Saved<BM>> {
815
- _assert(bm.id, 'patch argument object should have an id', {
816
- bm,
817
- })
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
+ }
818
841
 
819
842
  const loaded = await this.getById(bm.id, opt)
820
843
 
@@ -835,6 +858,19 @@ export class CommonDao<
835
858
  return await this.save(bm, opt)
836
859
  }
837
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
+
838
874
  async saveAsDBM(dbm: DBM, opt: CommonDaoSaveBatchOptions<DBM> = {}): Promise<Saved<DBM>> {
839
875
  this.requireWriteAccess()
840
876
  const table = opt.table || this.cfg.table
@@ -843,7 +879,7 @@ export class CommonDao<
843
879
  // will override/set `updated` field, unless opts.preserveUpdated is set
844
880
  let row = dbm as Saved<DBM>
845
881
  if (!opt.raw) {
846
- const idWasGenerated = !dbm.id && this.cfg.createId
882
+ const idWasGenerated = !dbm.id && this.cfg.generateId
847
883
  this.assignIdCreatedUpdated(dbm, opt) // mutates
848
884
  row = this.anyToDBM(dbm, opt)
849
885
  if (opt.ensureUniqueId && idWasGenerated) await this.ensureUniqueId(table, row)
@@ -861,7 +897,7 @@ export class CommonDao<
861
897
  if (row === null) return dbm as Saved<DBM>
862
898
  }
863
899
 
864
- await this.cfg.db.saveBatch(table, [row], {
900
+ await (opt.tx || this.cfg.db).saveBatch(table, [row], {
865
901
  excludeFromIndexes,
866
902
  assignGeneratedIds,
867
903
  ...opt,
@@ -952,7 +988,7 @@ export class CommonDao<
952
988
  )
953
989
  }
954
990
 
955
- await this.cfg.db.saveBatch(table, rows, {
991
+ await (opt.tx || this.cfg.db).saveBatch(table, rows, {
956
992
  excludeFromIndexes,
957
993
  assignGeneratedIds,
958
994
  ...opt,
@@ -1163,7 +1199,7 @@ export class CommonDao<
1163
1199
  const bm = await this.cfg.hooks!.beforeDBMToBM!(dbm)
1164
1200
 
1165
1201
  // Validate/convert BM
1166
- // eslint-disable-next-line @typescript-eslint/return-await
1202
+
1167
1203
  return this.validateAndConvert(bm, this.cfg.bmSchema, DBModelType.BM, opt)
1168
1204
  }
1169
1205
 
@@ -1192,7 +1228,7 @@ export class CommonDao<
1192
1228
  const dbm = { ...(await this.cfg.hooks!.beforeBMToDBM!(bm)) }
1193
1229
 
1194
1230
  // Validate/convert DBM
1195
- // eslint-disable-next-line @typescript-eslint/return-await
1231
+
1196
1232
  return this.validateAndConvert(dbm, this.cfg.dbmSchema, DBModelType.DBM, opt)
1197
1233
  }
1198
1234
 
@@ -1251,14 +1287,14 @@ export class CommonDao<
1251
1287
  *
1252
1288
  * Does NOT mutate the object.
1253
1289
  */
1254
- validateAndConvert<IN, OUT = IN>(
1255
- obj: Partial<IN>,
1256
- schema: ObjectSchema<IN> | AjvSchema<IN> | ZodSchema<IN> | undefined,
1290
+ validateAndConvert<T>(
1291
+ obj: Partial<T>,
1292
+ schema: ObjectSchema<T> | AjvSchema<T> | ZodSchema<T> | undefined,
1257
1293
  modelType: DBModelType,
1258
1294
  opt: CommonDaoOptions = {},
1259
- ): OUT {
1295
+ ): any {
1260
1296
  // `raw` option completely bypasses any processing
1261
- if (opt.raw) return obj as any as OUT
1297
+ if (opt.raw) return obj as any
1262
1298
 
1263
1299
  // Kirill 2021-10-18: I realized that there's little reason to keep removing null values
1264
1300
  // So, from now on we'll preserve them
@@ -1277,31 +1313,31 @@ export class CommonDao<
1277
1313
 
1278
1314
  // Pre-validation hooks
1279
1315
  if (modelType === DBModelType.DBM) {
1280
- obj = this.cfg.hooks!.beforeDBMValidate!(obj as any) as IN
1316
+ obj = this.cfg.hooks!.beforeDBMValidate!(obj as any) as T
1281
1317
  }
1282
1318
 
1283
1319
  // Return as is if no schema is passed or if `skipConversion` is set
1284
1320
  if (!schema || opt.skipConversion) {
1285
- return obj as OUT
1321
+ return obj
1286
1322
  }
1287
1323
 
1288
1324
  // This will Convert and Validate
1289
1325
  const table = opt.table || this.cfg.table
1290
1326
  const objectName = table + (modelType || '')
1291
1327
 
1292
- let error: JoiValidationError | AjvValidationError | ZodValidationError<IN> | undefined
1328
+ let error: JoiValidationError | AjvValidationError | ZodValidationError<T> | undefined
1293
1329
  let convertedValue: any
1294
1330
 
1295
1331
  if (schema instanceof ZodSchema) {
1296
1332
  // Zod schema
1297
- const vr = zSafeValidate(obj as IN, schema)
1333
+ const vr = zSafeValidate(obj as T, schema)
1298
1334
  error = vr.error
1299
1335
  convertedValue = vr.data
1300
1336
  } else if (schema instanceof AjvSchema) {
1301
1337
  // Ajv schema
1302
1338
  convertedValue = obj // because Ajv mutates original object
1303
1339
 
1304
- error = schema.getValidationError(obj as IN, {
1340
+ error = schema.getValidationError(obj as T, {
1305
1341
  objectName,
1306
1342
  })
1307
1343
  } else {
@@ -1337,20 +1373,24 @@ export class CommonDao<
1337
1373
  await this.cfg.db.ping()
1338
1374
  }
1339
1375
 
1340
- async runInTransaction(
1341
- fn: CommonDaoTransactionFn,
1376
+ async runInTransaction<T = void>(
1377
+ fn: CommonDaoTransactionFn<T>,
1342
1378
  opt?: CommonDBTransactionOptions,
1343
- ): Promise<void> {
1379
+ ): Promise<T> {
1380
+ let r: T
1381
+
1344
1382
  await this.cfg.db.runInTransaction(async tx => {
1345
1383
  const daoTx = new CommonDaoTransaction(tx, this.cfg.logger!)
1346
1384
 
1347
1385
  try {
1348
- await fn(daoTx)
1386
+ r = await fn(daoTx)
1349
1387
  } catch (err) {
1350
1388
  await daoTx.rollback() // graceful rollback that "never throws"
1351
1389
  throw err
1352
1390
  }
1353
1391
  }, opt)
1392
+
1393
+ return r!
1354
1394
  }
1355
1395
 
1356
1396
  protected logResult(started: number, op: string, res: any, table: string): void {
@@ -1415,7 +1455,7 @@ export class CommonDao<
1415
1455
  *
1416
1456
  * Transaction is rolled back when the function returns rejected Promise (aka "throws").
1417
1457
  */
1418
- export type CommonDaoTransactionFn = (tx: CommonDaoTransaction) => Promise<void>
1458
+ export type CommonDaoTransactionFn<T = void> = (tx: CommonDaoTransaction) => Promise<T>
1419
1459
 
1420
1460
  /**
1421
1461
  * Transaction context.
@@ -1423,7 +1463,7 @@ export type CommonDaoTransactionFn = (tx: CommonDaoTransaction) => Promise<void>
1423
1463
  */
1424
1464
  export class CommonDaoTransaction {
1425
1465
  constructor(
1426
- private tx: DBTransaction,
1466
+ public tx: DBTransaction,
1427
1467
  private logger: CommonLogger,
1428
1468
  ) {}
1429
1469
 
@@ -1444,8 +1484,7 @@ export class CommonDaoTransaction {
1444
1484
  id?: string | null,
1445
1485
  opt?: CommonDaoOptions,
1446
1486
  ): Promise<Saved<BM> | null> {
1447
- if (!id) return null
1448
- return (await this.getByIds(dao, [id], opt))[0] || null
1487
+ return await dao.getById(id, { ...opt, tx: this.tx })
1449
1488
  }
1450
1489
 
1451
1490
  async getByIds<BM extends PartialObjectWithId, DBM extends PartialObjectWithId>(