@naturalcycles/db-lib 9.6.0 → 9.7.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.
@@ -19,7 +19,6 @@ export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity
19
19
  getById(id: undefined | null, opt?: CommonDaoOptions): Promise<null>;
20
20
  getById(id?: string | null, opt?: CommonDaoOptions): Promise<BM | null>;
21
21
  getByIdOrEmpty(id: string, part?: Partial<BM>, opt?: CommonDaoOptions): Promise<BM>;
22
- getByIdAsDBMOrEmpty(id: string, part?: Partial<BM>, opt?: CommonDaoOptions): Promise<DBM>;
23
22
  getByIdAsDBM(id: undefined | null, opt?: CommonDaoOptions): Promise<null>;
24
23
  getByIdAsDBM(id?: string | null, opt?: CommonDaoOptions): Promise<DBM | null>;
25
24
  getByIds(ids: string[], opt?: CommonDaoOptions): Promise<BM[]>;
@@ -79,10 +78,6 @@ export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity
79
78
  * "Returns", just to have a type of "Saved"
80
79
  */
81
80
  assignIdCreatedUpdated<T extends BaseDBEntity>(obj: Partial<T>, opt?: CommonDaoOptions): T;
82
- /**
83
- * Mutates with id, created, updated
84
- */
85
- save(bm: Unsaved<BM>, opt?: CommonDaoSaveOptions<BM, DBM>): Promise<BM>;
86
81
  /**
87
82
  * 1. Applies the patch
88
83
  * 2. If object is the same after patching - skips saving it
@@ -117,7 +112,11 @@ export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity
117
112
  * Like patch, but runs all operations within a Transaction.
118
113
  */
119
114
  patchInTransaction(bm: BM, patch: Partial<BM>, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<BM>;
120
- saveAsDBM(dbm: Unsaved<DBM>, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<DBM>;
115
+ /**
116
+ * Mutates with id, created, updated
117
+ */
118
+ save(bm: Unsaved<BM>, opt?: CommonDaoSaveOptions<BM, DBM>): Promise<BM>;
119
+ saveAsDBM(dbm: Unsaved<DBM>, opt?: CommonDaoSaveOptions<BM, DBM>): Promise<DBM>;
121
120
  saveBatch(bms: Unsaved<BM>[], opt?: CommonDaoSaveBatchOptions<DBM>): Promise<BM[]>;
122
121
  saveBatchAsDBM(dbms: Unsaved<DBM>[], opt?: CommonDaoSaveBatchOptions<DBM>): Promise<DBM[]>;
123
122
  /**
@@ -130,7 +129,6 @@ export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity
130
129
  /**
131
130
  * @returns number of deleted items
132
131
  */
133
- deleteById(id: undefined | null, opt?: CommonDaoOptions): Promise<0>;
134
132
  deleteById(id?: string | null, opt?: CommonDaoOptions): Promise<number>;
135
133
  deleteByIds(ids: string[], opt?: CommonDaoOptions): Promise<number>;
136
134
  /**
@@ -156,10 +154,9 @@ export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity
156
154
  anyToDBM(dbm?: any, opt?: CommonDaoOptions): DBM;
157
155
  anyToDBMs(entities: DBM[], opt?: CommonDaoOptions): DBM[];
158
156
  /**
159
- * Returns *converted value*.
160
- * Validates (unless `skipValidation=true` passed).
161
- *
157
+ * Returns *converted value* (NOT the same reference).
162
158
  * Does NOT mutate the object.
159
+ * Validates (unless `skipValidation=true` passed).
163
160
  */
164
161
  validateAndConvert<T>(obj: Partial<T>, schema: ObjectSchema<T> | AjvSchema<T> | ZodSchema<T> | undefined, opt?: CommonDaoOptions): any;
165
162
  getTableSchema(): Promise<JsonSchemaRootObject<DBM>>;
@@ -194,8 +191,16 @@ export declare class CommonDaoTransaction {
194
191
  rollback(): Promise<void>;
195
192
  getById<BM extends BaseDBEntity, DBM extends BaseDBEntity>(dao: CommonDao<BM, DBM>, id?: string | null, opt?: CommonDaoOptions): Promise<BM | null>;
196
193
  getByIds<BM extends BaseDBEntity, DBM extends BaseDBEntity>(dao: CommonDao<BM, DBM>, ids: string[], opt?: CommonDaoOptions): Promise<BM[]>;
197
- save<BM extends BaseDBEntity, DBM extends BaseDBEntity>(dao: CommonDao<BM, DBM>, bm: Unsaved<BM>, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<BM>;
194
+ save<BM extends BaseDBEntity, DBM extends BaseDBEntity>(dao: CommonDao<BM, DBM>, bm: Unsaved<BM>, opt?: CommonDaoSaveOptions<BM, DBM>): Promise<BM>;
198
195
  saveBatch<BM extends BaseDBEntity, DBM extends BaseDBEntity>(dao: CommonDao<BM, DBM>, bms: Unsaved<BM>[], opt?: CommonDaoSaveBatchOptions<DBM>): Promise<BM[]>;
196
+ /**
197
+ * DaoTransaction.patch does not load from DB.
198
+ * It assumes the bm was previously loaded in the same Transaction, hence could not be
199
+ * concurrently modified. Hence it's safe to not sync with DB.
200
+ *
201
+ * So, this method is a rather simple convenience "Object.assign and then save".
202
+ */
203
+ patch<BM extends BaseDBEntity, DBM extends BaseDBEntity>(dao: CommonDao<BM, DBM>, bm: BM, patch: Partial<BM>, opt?: CommonDaoSaveOptions<BM, DBM>): Promise<BM>;
199
204
  deleteById(dao: CommonDao<any>, id?: string | null, opt?: CommonDaoOptions): Promise<number>;
200
205
  deleteByIds(dao: CommonDao<any>, ids: string[], opt?: CommonDaoOptions): Promise<number>;
201
206
  }
@@ -71,13 +71,6 @@ class CommonDao {
71
71
  return bm;
72
72
  return this.create({ ...part, id }, opt);
73
73
  }
74
- async getByIdAsDBMOrEmpty(id, part = {}, opt) {
75
- const dbm = await this.getByIdAsDBM(id, opt);
76
- if (dbm)
77
- return dbm;
78
- const bm = this.create({ ...part, id }, opt);
79
- return await this.bmToDBM(bm, opt);
80
- }
81
74
  async getByIdAsDBM(id, opt = {}) {
82
75
  if (!id)
83
76
  return null;
@@ -451,45 +444,6 @@ class CommonDao {
451
444
  return obj;
452
445
  }
453
446
  // SAVE
454
- /**
455
- * Mutates with id, created, updated
456
- */
457
- async save(bm, opt = {}) {
458
- this.requireWriteAccess();
459
- if (opt.skipIfEquals && (0, js_lib_1._deepJsonEquals)(bm, opt.skipIfEquals)) {
460
- // Skipping the save operation
461
- return bm;
462
- }
463
- const idWasGenerated = !bm.id && this.cfg.generateId;
464
- this.assignIdCreatedUpdated(bm, opt); // mutates
465
- (0, js_lib_1._typeCast)(bm);
466
- let dbm = await this.bmToDBM(bm, opt); // validates BM
467
- if (this.cfg.hooks.beforeSave) {
468
- dbm = (await this.cfg.hooks.beforeSave(dbm));
469
- if (dbm === null)
470
- return bm;
471
- }
472
- const table = opt.table || this.cfg.table;
473
- if (opt.ensureUniqueId && idWasGenerated)
474
- await this.ensureUniqueId(table, dbm);
475
- if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
476
- opt = { ...opt, saveMethod: 'insert' };
477
- }
478
- const op = `save(${dbm.id})`;
479
- const started = this.logSaveStarted(op, bm, table);
480
- const { excludeFromIndexes } = this.cfg;
481
- const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds;
482
- await (opt.tx || this.cfg.db).saveBatch(table, [dbm], {
483
- excludeFromIndexes,
484
- assignGeneratedIds,
485
- ...opt,
486
- });
487
- if (assignGeneratedIds) {
488
- bm.id = dbm.id;
489
- }
490
- this.logSaveResult(started, op, table);
491
- return bm;
492
- }
493
447
  /**
494
448
  * 1. Applies the patch
495
449
  * 2. If object is the same after patching - skips saving it
@@ -585,6 +539,51 @@ class CommonDao {
585
539
  return await this.patch(bm, patch, { ...opt, tx: daoTx.tx });
586
540
  });
587
541
  }
542
+ /**
543
+ * Mutates with id, created, updated
544
+ */
545
+ async save(bm, opt = {}) {
546
+ this.requireWriteAccess();
547
+ if (opt.skipIfEquals) {
548
+ // We compare with convertedBM, to account for cases when some extra property is assigned to bm,
549
+ // which should be removed post-validation, but it breaks the "equality check"
550
+ // Post-validation the equality check should work as intended
551
+ const convertedBM = this.validateAndConvert(bm, this.cfg.bmSchema, opt);
552
+ if ((0, js_lib_1._deepJsonEquals)(convertedBM, opt.skipIfEquals)) {
553
+ // Skipping the save operation
554
+ return bm;
555
+ }
556
+ }
557
+ const idWasGenerated = !bm.id && this.cfg.generateId;
558
+ this.assignIdCreatedUpdated(bm, opt); // mutates
559
+ (0, js_lib_1._typeCast)(bm);
560
+ let dbm = await this.bmToDBM(bm, opt); // validates BM
561
+ if (this.cfg.hooks.beforeSave) {
562
+ dbm = (await this.cfg.hooks.beforeSave(dbm));
563
+ if (dbm === null)
564
+ return bm;
565
+ }
566
+ const table = opt.table || this.cfg.table;
567
+ if (opt.ensureUniqueId && idWasGenerated)
568
+ await this.ensureUniqueId(table, dbm);
569
+ if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
570
+ opt = { ...opt, saveMethod: 'insert' };
571
+ }
572
+ const op = `save(${dbm.id})`;
573
+ const started = this.logSaveStarted(op, bm, table);
574
+ const { excludeFromIndexes } = this.cfg;
575
+ const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds;
576
+ await (opt.tx || this.cfg.db).saveBatch(table, [dbm], {
577
+ excludeFromIndexes,
578
+ assignGeneratedIds,
579
+ ...opt,
580
+ });
581
+ if (assignGeneratedIds) {
582
+ bm.id = dbm.id;
583
+ }
584
+ this.logSaveResult(started, op, table);
585
+ return bm;
586
+ }
588
587
  async saveAsDBM(dbm, opt = {}) {
589
588
  this.requireWriteAccess();
590
589
  const table = opt.table || this.cfg.table;
@@ -736,17 +735,14 @@ class CommonDao {
736
735
  (0, nodejs_lib_1.writableVoid)(),
737
736
  ];
738
737
  }
738
+ // DELETE
739
+ /**
740
+ * @returns number of deleted items
741
+ */
739
742
  async deleteById(id, opt = {}) {
740
743
  if (!id)
741
744
  return 0;
742
- this.requireWriteAccess();
743
- this.requireObjectMutability(opt);
744
- const op = `deleteById(${id})`;
745
- const table = opt.table || this.cfg.table;
746
- const started = this.logStarted(op, table);
747
- const count = await this.cfg.db.deleteByIds(table, [id], opt);
748
- this.logSaveResult(started, op, table);
749
- return count;
745
+ return await this.deleteByIds([id], opt);
750
746
  }
751
747
  async deleteByIds(ids, opt = {}) {
752
748
  if (!ids.length)
@@ -830,13 +826,7 @@ class CommonDao {
830
826
  dbm = this.cfg.hooks.anonymize(dbm);
831
827
  }
832
828
  // DBM > BM
833
- let bm;
834
- if (this.cfg.hooks.beforeDBMToBM) {
835
- bm = await this.cfg.hooks.beforeDBMToBM(dbm);
836
- }
837
- else {
838
- bm = dbm;
839
- }
829
+ const bm = ((await this.cfg.hooks.beforeDBMToBM?.(dbm)) || dbm);
840
830
  // Validate/convert BM
841
831
  return this.validateAndConvert(bm, this.cfg.bmSchema, opt);
842
832
  }
@@ -846,21 +836,10 @@ class CommonDao {
846
836
  async bmToDBM(bm, opt) {
847
837
  if (bm === undefined)
848
838
  return;
849
- // should not do it on load, but only on save!
850
- // this.assignIdCreatedUpdated(bm, opt)
851
839
  // bm gets assigned to the new reference
852
840
  bm = this.validateAndConvert(bm, this.cfg.bmSchema, opt);
853
841
  // BM > DBM
854
- let dbm;
855
- if (this.cfg.hooks.beforeBMToDBM) {
856
- dbm = { ...(await this.cfg.hooks.beforeBMToDBM(bm)) };
857
- }
858
- else {
859
- dbm = bm;
860
- }
861
- // Validate/convert DBM
862
- // return this.validateAndConvert(dbm, this.cfg.dbmSchema, DBModelType.DBM, opt)
863
- return dbm;
842
+ return ((await this.cfg.hooks.beforeBMToDBM?.(bm)) || bm);
864
843
  }
865
844
  async bmsToDBM(bms, opt = {}) {
866
845
  // try/catch?
@@ -885,10 +864,9 @@ class CommonDao {
885
864
  return entities.map(entity => this.anyToDBM(entity, opt));
886
865
  }
887
866
  /**
888
- * Returns *converted value*.
889
- * Validates (unless `skipValidation=true` passed).
890
- *
867
+ * Returns *converted value* (NOT the same reference).
891
868
  * Does NOT mutate the object.
869
+ * Validates (unless `skipValidation=true` passed).
892
870
  */
893
871
  validateAndConvert(obj, schema, opt = {}) {
894
872
  // Kirill 2021-10-18: I realized that there's little reason to keep removing null values
@@ -1060,11 +1038,23 @@ class CommonDaoTransaction {
1060
1038
  // }
1061
1039
  // }
1062
1040
  async save(dao, bm, opt) {
1063
- return (await this.saveBatch(dao, [bm], opt))[0];
1041
+ return await dao.save(bm, { ...opt, tx: this.tx });
1064
1042
  }
1065
1043
  async saveBatch(dao, bms, opt) {
1066
1044
  return await dao.saveBatch(bms, { ...opt, tx: this.tx });
1067
1045
  }
1046
+ /**
1047
+ * DaoTransaction.patch does not load from DB.
1048
+ * It assumes the bm was previously loaded in the same Transaction, hence could not be
1049
+ * concurrently modified. Hence it's safe to not sync with DB.
1050
+ *
1051
+ * So, this method is a rather simple convenience "Object.assign and then save".
1052
+ */
1053
+ async patch(dao, bm, patch, opt) {
1054
+ const skipIfEquals = (0, js_lib_1._deepCopy)(bm);
1055
+ Object.assign(bm, patch);
1056
+ return await dao.save(bm, { ...opt, skipIfEquals, tx: this.tx });
1057
+ }
1068
1058
  async deleteById(dao, id, opt) {
1069
1059
  if (!id)
1070
1060
  return 0;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/db-lib",
3
3
  "scripts": {
4
- "prepare": "husky install"
4
+ "prepare": "husky"
5
5
  },
6
6
  "dependencies": {
7
7
  "@naturalcycles/js-lib": "^14.116.0",
@@ -40,7 +40,7 @@
40
40
  "engines": {
41
41
  "node": ">=18.12"
42
42
  },
43
- "version": "9.6.0",
43
+ "version": "9.7.1",
44
44
  "description": "Lowest Common Denominator API to supported Databases",
45
45
  "keywords": [
46
46
  "db",
@@ -1,6 +1,7 @@
1
1
  import { Transform } from 'node:stream'
2
2
  import {
3
3
  _assert,
4
+ _deepCopy,
4
5
  _deepJsonEquals,
5
6
  _filterUndefinedValues,
6
7
  _isTruthy,
@@ -131,18 +132,6 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
131
132
  return this.create({ ...part, id }, opt)
132
133
  }
133
134
 
134
- async getByIdAsDBMOrEmpty(
135
- id: string,
136
- part: Partial<BM> = {},
137
- opt?: CommonDaoOptions,
138
- ): Promise<DBM> {
139
- const dbm = await this.getByIdAsDBM(id, opt)
140
- if (dbm) return dbm
141
-
142
- const bm = this.create({ ...part, id }, opt)
143
- return await this.bmToDBM(bm, opt)
144
- }
145
-
146
135
  async getByIdAsDBM(id: undefined | null, opt?: CommonDaoOptions): Promise<null>
147
136
  async getByIdAsDBM(id?: string | null, opt?: CommonDaoOptions): Promise<DBM | null>
148
137
  async getByIdAsDBM(id?: string | null, opt: CommonDaoOptions = {}): Promise<DBM | null> {
@@ -615,51 +604,6 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
615
604
  }
616
605
 
617
606
  // SAVE
618
- /**
619
- * Mutates with id, created, updated
620
- */
621
- async save(bm: Unsaved<BM>, opt: CommonDaoSaveOptions<BM, DBM> = {}): Promise<BM> {
622
- this.requireWriteAccess()
623
-
624
- if (opt.skipIfEquals && _deepJsonEquals(bm, opt.skipIfEquals)) {
625
- // Skipping the save operation
626
- return bm as BM
627
- }
628
-
629
- const idWasGenerated = !bm.id && this.cfg.generateId
630
- this.assignIdCreatedUpdated(bm, opt) // mutates
631
- _typeCast<BM>(bm)
632
- let dbm = await this.bmToDBM(bm, opt) // validates BM
633
-
634
- if (this.cfg.hooks!.beforeSave) {
635
- dbm = (await this.cfg.hooks!.beforeSave(dbm))!
636
- if (dbm === null) return bm
637
- }
638
-
639
- const table = opt.table || this.cfg.table
640
- if (opt.ensureUniqueId && idWasGenerated) await this.ensureUniqueId(table, dbm)
641
- if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
642
- opt = { ...opt, saveMethod: 'insert' }
643
- }
644
- const op = `save(${dbm.id})`
645
- const started = this.logSaveStarted(op, bm, table)
646
- const { excludeFromIndexes } = this.cfg
647
- const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds
648
-
649
- await (opt.tx || this.cfg.db).saveBatch(table, [dbm], {
650
- excludeFromIndexes,
651
- assignGeneratedIds,
652
- ...opt,
653
- })
654
-
655
- if (assignGeneratedIds) {
656
- bm.id = dbm.id
657
- }
658
-
659
- this.logSaveResult(started, op, table)
660
- return bm
661
- }
662
-
663
607
  /**
664
608
  * 1. Applies the patch
665
609
  * 2. If object is the same after patching - skips saving it
@@ -782,7 +726,58 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
782
726
  })
783
727
  }
784
728
 
785
- async saveAsDBM(dbm: Unsaved<DBM>, opt: CommonDaoSaveBatchOptions<DBM> = {}): Promise<DBM> {
729
+ /**
730
+ * Mutates with id, created, updated
731
+ */
732
+ async save(bm: Unsaved<BM>, opt: CommonDaoSaveOptions<BM, DBM> = {}): Promise<BM> {
733
+ this.requireWriteAccess()
734
+
735
+ if (opt.skipIfEquals) {
736
+ // We compare with convertedBM, to account for cases when some extra property is assigned to bm,
737
+ // which should be removed post-validation, but it breaks the "equality check"
738
+ // Post-validation the equality check should work as intended
739
+ const convertedBM = this.validateAndConvert(bm as Partial<BM>, this.cfg.bmSchema, opt)
740
+ if (_deepJsonEquals(convertedBM, opt.skipIfEquals)) {
741
+ // Skipping the save operation
742
+ return bm as BM
743
+ }
744
+ }
745
+
746
+ const idWasGenerated = !bm.id && this.cfg.generateId
747
+ this.assignIdCreatedUpdated(bm, opt) // mutates
748
+ _typeCast<BM>(bm)
749
+ let dbm = await this.bmToDBM(bm, opt) // validates BM
750
+
751
+ if (this.cfg.hooks!.beforeSave) {
752
+ dbm = (await this.cfg.hooks!.beforeSave(dbm))!
753
+ if (dbm === null) return bm
754
+ }
755
+
756
+ const table = opt.table || this.cfg.table
757
+ if (opt.ensureUniqueId && idWasGenerated) await this.ensureUniqueId(table, dbm)
758
+ if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
759
+ opt = { ...opt, saveMethod: 'insert' }
760
+ }
761
+ const op = `save(${dbm.id})`
762
+ const started = this.logSaveStarted(op, bm, table)
763
+ const { excludeFromIndexes } = this.cfg
764
+ const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds
765
+
766
+ await (opt.tx || this.cfg.db).saveBatch(table, [dbm], {
767
+ excludeFromIndexes,
768
+ assignGeneratedIds,
769
+ ...opt,
770
+ })
771
+
772
+ if (assignGeneratedIds) {
773
+ bm.id = dbm.id
774
+ }
775
+
776
+ this.logSaveResult(started, op, table)
777
+ return bm
778
+ }
779
+
780
+ async saveAsDBM(dbm: Unsaved<DBM>, opt: CommonDaoSaveOptions<BM, DBM> = {}): Promise<DBM> {
786
781
  this.requireWriteAccess()
787
782
  const table = opt.table || this.cfg.table
788
783
 
@@ -979,18 +974,9 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
979
974
  /**
980
975
  * @returns number of deleted items
981
976
  */
982
- async deleteById(id: undefined | null, opt?: CommonDaoOptions): Promise<0>
983
- async deleteById(id?: string | null, opt?: CommonDaoOptions): Promise<number>
984
977
  async deleteById(id?: string | null, opt: CommonDaoOptions = {}): Promise<number> {
985
978
  if (!id) return 0
986
- this.requireWriteAccess()
987
- this.requireObjectMutability(opt)
988
- const op = `deleteById(${id})`
989
- const table = opt.table || this.cfg.table
990
- const started = this.logStarted(op, table)
991
- const count = await this.cfg.db.deleteByIds(table, [id], opt)
992
- this.logSaveResult(started, op, table)
993
- return count
979
+ return await this.deleteByIds([id], opt)
994
980
  }
995
981
 
996
982
  async deleteByIds(ids: string[], opt: CommonDaoOptions = {}): Promise<number> {
@@ -1103,15 +1089,9 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
1103
1089
  }
1104
1090
 
1105
1091
  // DBM > BM
1106
- let bm: Partial<BM>
1107
- if (this.cfg.hooks!.beforeDBMToBM) {
1108
- bm = await this.cfg.hooks!.beforeDBMToBM(dbm)
1109
- } else {
1110
- bm = dbm as any
1111
- }
1092
+ const bm = ((await this.cfg.hooks!.beforeDBMToBM?.(dbm)) || dbm) as Partial<BM>
1112
1093
 
1113
1094
  // Validate/convert BM
1114
-
1115
1095
  return this.validateAndConvert(bm, this.cfg.bmSchema, opt)
1116
1096
  }
1117
1097
 
@@ -1128,23 +1108,11 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
1128
1108
  async bmToDBM(bm?: BM, opt?: CommonDaoOptions): Promise<DBM | undefined> {
1129
1109
  if (bm === undefined) return
1130
1110
 
1131
- // should not do it on load, but only on save!
1132
- // this.assignIdCreatedUpdated(bm, opt)
1133
-
1134
1111
  // bm gets assigned to the new reference
1135
1112
  bm = this.validateAndConvert(bm, this.cfg.bmSchema, opt)
1136
1113
 
1137
1114
  // BM > DBM
1138
- let dbm: DBM
1139
- if (this.cfg.hooks!.beforeBMToDBM) {
1140
- dbm = { ...((await this.cfg.hooks!.beforeBMToDBM(bm!)) as DBM) }
1141
- } else {
1142
- dbm = bm as any
1143
- }
1144
-
1145
- // Validate/convert DBM
1146
- // return this.validateAndConvert(dbm, this.cfg.dbmSchema, DBModelType.DBM, opt)
1147
- return dbm
1115
+ return ((await this.cfg.hooks!.beforeBMToDBM?.(bm!)) || bm) as DBM
1148
1116
  }
1149
1117
 
1150
1118
  async bmsToDBM(bms: BM[], opt: CommonDaoOptions = {}): Promise<DBM[]> {
@@ -1178,10 +1146,9 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM> {
1178
1146
  }
1179
1147
 
1180
1148
  /**
1181
- * Returns *converted value*.
1182
- * Validates (unless `skipValidation=true` passed).
1183
- *
1149
+ * Returns *converted value* (NOT the same reference).
1184
1150
  * Does NOT mutate the object.
1151
+ * Validates (unless `skipValidation=true` passed).
1185
1152
  */
1186
1153
  validateAndConvert<T>(
1187
1154
  obj: Partial<T>,
@@ -1395,9 +1362,9 @@ export class CommonDaoTransaction {
1395
1362
  async save<BM extends BaseDBEntity, DBM extends BaseDBEntity>(
1396
1363
  dao: CommonDao<BM, DBM>,
1397
1364
  bm: Unsaved<BM>,
1398
- opt?: CommonDaoSaveBatchOptions<DBM>,
1365
+ opt?: CommonDaoSaveOptions<BM, DBM>,
1399
1366
  ): Promise<BM> {
1400
- return (await this.saveBatch(dao, [bm], opt))[0]!
1367
+ return await dao.save(bm, { ...opt, tx: this.tx })
1401
1368
  }
1402
1369
 
1403
1370
  async saveBatch<BM extends BaseDBEntity, DBM extends BaseDBEntity>(
@@ -1408,6 +1375,24 @@ export class CommonDaoTransaction {
1408
1375
  return await dao.saveBatch(bms, { ...opt, tx: this.tx })
1409
1376
  }
1410
1377
 
1378
+ /**
1379
+ * DaoTransaction.patch does not load from DB.
1380
+ * It assumes the bm was previously loaded in the same Transaction, hence could not be
1381
+ * concurrently modified. Hence it's safe to not sync with DB.
1382
+ *
1383
+ * So, this method is a rather simple convenience "Object.assign and then save".
1384
+ */
1385
+ async patch<BM extends BaseDBEntity, DBM extends BaseDBEntity>(
1386
+ dao: CommonDao<BM, DBM>,
1387
+ bm: BM,
1388
+ patch: Partial<BM>,
1389
+ opt?: CommonDaoSaveOptions<BM, DBM>,
1390
+ ): Promise<BM> {
1391
+ const skipIfEquals = _deepCopy(bm)
1392
+ Object.assign(bm, patch)
1393
+ return await dao.save(bm, { ...opt, skipIfEquals, tx: this.tx })
1394
+ }
1395
+
1411
1396
  async deleteById(
1412
1397
  dao: CommonDao<any>,
1413
1398
  id?: string | null,