@naturalcycles/db-lib 8.55.1 → 8.57.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.
@@ -4,7 +4,7 @@ import { AnyObject, AsyncMapper, JsonSchemaObject, JsonSchemaRootObject, ObjectW
4
4
  import { AjvSchema, ObjectSchema, ReadableTyped } from '@naturalcycles/nodejs-lib';
5
5
  import { DBDeleteByIdsOperation, DBModelType, DBOperation, DBPatch, DBSaveBatchOperation, RunQueryResult } from '../db.model';
6
6
  import { DBQuery, RunnableDBQuery } from '../query/dbQuery';
7
- import { CommonDaoCfg, CommonDaoCreateOptions, CommonDaoOptions, CommonDaoSaveOptions, CommonDaoStreamDeleteOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions, CommonDaoStreamSaveOptions } from './common.dao.model';
7
+ import { CommonDaoCfg, CommonDaoCreateOptions, CommonDaoOptions, CommonDaoSaveBatchOptions, CommonDaoSaveOptions, CommonDaoStreamDeleteOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions, CommonDaoStreamSaveOptions } from './common.dao.model';
8
8
  /**
9
9
  * Lowest common denominator API between supported Databases.
10
10
  *
@@ -86,28 +86,44 @@ export declare class CommonDao<BM extends Partial<ObjectWithId<ID>>, DBM extends
86
86
  assignIdCreatedUpdated(obj: BM, opt?: CommonDaoOptions): Saved<BM>;
87
87
  assignIdCreatedUpdated(obj: Unsaved<BM>, opt?: CommonDaoOptions): Saved<BM>;
88
88
  tx: {
89
- save: (bm: Unsaved<BM>, opt?: CommonDaoSaveOptions<DBM>) => Promise<DBSaveBatchOperation | undefined>;
90
- saveBatch: (bms: Unsaved<BM>[], opt?: CommonDaoSaveOptions<DBM>) => Promise<DBSaveBatchOperation | undefined>;
89
+ save: (bm: Unsaved<BM>, opt?: CommonDaoSaveBatchOptions<DBM>) => Promise<DBSaveBatchOperation | undefined>;
90
+ saveBatch: (bms: Unsaved<BM>[], opt?: CommonDaoSaveBatchOptions<DBM>) => Promise<DBSaveBatchOperation | undefined>;
91
91
  deleteByIds: (ids: ID[], opt?: CommonDaoOptions) => Promise<DBDeleteByIdsOperation | undefined>;
92
92
  deleteById: (id: ID | null | undefined, opt?: CommonDaoOptions) => Promise<DBDeleteByIdsOperation | undefined>;
93
93
  };
94
94
  /**
95
95
  * Mutates with id, created, updated
96
96
  */
97
- save(bm: Unsaved<BM>, opt?: CommonDaoSaveOptions<DBM>): Promise<Saved<BM>>;
97
+ save(bm: Unsaved<BM>, opt?: CommonDaoSaveOptions<BM, DBM>): Promise<Saved<BM>>;
98
98
  /**
99
- * Loads the row by id.
100
- * Creates the row (via this.create()) if it doesn't exist
101
- * (this will cause a validation error if Patch has not enough data for the row to be valid).
102
- * Saves (as fast as possible) with the Patch applied.
99
+ * 1. Applies the patch
100
+ * 2. If object is the same after patching - skips saving it
101
+ * 3. Otherwise - saves the patched object and returns it
102
+ *
103
+ * Similar to `save` with skipIfEquals.
104
+ * Similar to `patch`, but doesn't load the object from the Database.
105
+ */
106
+ savePatch(bm: Saved<BM>, patch: Partial<BM>, opt: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>>;
107
+ /**
108
+ * Convenience method to replace 3 operations (loading+patching+saving) with one:
103
109
  *
104
- * Convenience method to replace 3 operations (loading+patching+saving) with one.
110
+ * 1. Loads the row by id.
111
+ * 1.1 Creates the row (via this.create()) if it doesn't exist
112
+ * (this will cause a validation error if Patch has not enough data for the row to be valid).
113
+ * 2. Applies the patch on top of loaded data.
114
+ * 3. Saves (as fast as possible since the read) with the Patch applied, but only if the data has changed.
115
+ */
116
+ patchById(id: ID, patch: Partial<BM>, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>>;
117
+ /**
118
+ * Same as patchById, but takes the whole object as input.
119
+ * This "whole object" is mutated with the patch and returned.
120
+ * Otherwise, similar behavior as patchById.
121
+ * It still loads the row from the DB.
105
122
  */
106
- patch(id: ID, patch: Partial<BM>, opt?: CommonDaoSaveOptions<DBM>): Promise<Saved<BM>>;
107
- patchAsDBM(id: ID, patch: Partial<DBM>, opt?: CommonDaoSaveOptions<DBM>): Promise<DBM>;
108
- saveAsDBM(dbm: DBM, opt?: CommonDaoSaveOptions<DBM>): Promise<DBM>;
109
- saveBatch(bms: Unsaved<BM>[], opt?: CommonDaoSaveOptions<DBM>): Promise<Saved<BM>[]>;
110
- saveBatchAsDBM(dbms: DBM[], opt?: CommonDaoSaveOptions<DBM>): Promise<DBM[]>;
123
+ patch(bm: Saved<BM>, patch: Partial<BM>, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>>;
124
+ saveAsDBM(dbm: DBM, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<DBM>;
125
+ saveBatch(bms: Unsaved<BM>[], opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>[]>;
126
+ saveBatchAsDBM(dbms: DBM[], opt?: CommonDaoSaveBatchOptions<DBM>): Promise<DBM[]>;
111
127
  /**
112
128
  * "Streaming" is implemented by buffering incoming rows into **batches**
113
129
  * (of size opt.batchSize, which defaults to 500),
@@ -555,6 +555,10 @@ class CommonDao {
555
555
  */
556
556
  async save(bm, opt = {}) {
557
557
  this.requireWriteAccess();
558
+ if (opt.skipIfEquals && (0, js_lib_1._deepJsonEquals)(bm, opt.skipIfEquals)) {
559
+ // Skipping the save operation
560
+ return bm;
561
+ }
558
562
  const idWasGenerated = !bm.id && this.cfg.createId;
559
563
  this.assignIdCreatedUpdated(bm, opt); // mutates
560
564
  let dbm = await this.bmToDBM(bm, opt);
@@ -589,26 +593,74 @@ class CommonDao {
589
593
  return bm;
590
594
  }
591
595
  /**
592
- * Loads the row by id.
593
- * Creates the row (via this.create()) if it doesn't exist
594
- * (this will cause a validation error if Patch has not enough data for the row to be valid).
595
- * Saves (as fast as possible) with the Patch applied.
596
+ * 1. Applies the patch
597
+ * 2. If object is the same after patching - skips saving it
598
+ * 3. Otherwise - saves the patched object and returns it
596
599
  *
597
- * Convenience method to replace 3 operations (loading+patching+saving) with one.
600
+ * Similar to `save` with skipIfEquals.
601
+ * Similar to `patch`, but doesn't load the object from the Database.
598
602
  */
599
- async patch(id, patch, opt = {}) {
600
- return await this.save({
601
- ...(await this.getByIdOrEmpty(id, patch, opt)),
603
+ async savePatch(bm, patch, opt) {
604
+ const patched = {
605
+ ...bm,
602
606
  ...patch,
603
- }, opt);
607
+ };
608
+ if ((0, js_lib_1._deepJsonEquals)(bm, patched)) {
609
+ // Skipping the save operation, as data is the same
610
+ return bm;
611
+ }
612
+ // Actually apply the patch by mutating the original object (by design)
613
+ Object.assign(bm, patch);
614
+ return await this.save(bm, opt);
604
615
  }
605
- async patchAsDBM(id, patch, opt = {}) {
606
- const dbm = (await this.getByIdAsDBM(id, opt)) ||
607
- this.create({ ...patch, id }, opt);
608
- return await this.saveAsDBM({
609
- ...dbm,
610
- ...patch,
611
- }, opt);
616
+ /**
617
+ * Convenience method to replace 3 operations (loading+patching+saving) with one:
618
+ *
619
+ * 1. Loads the row by id.
620
+ * 1.1 Creates the row (via this.create()) if it doesn't exist
621
+ * (this will cause a validation error if Patch has not enough data for the row to be valid).
622
+ * 2. Applies the patch on top of loaded data.
623
+ * 3. Saves (as fast as possible since the read) with the Patch applied, but only if the data has changed.
624
+ */
625
+ async patchById(id, patch, opt = {}) {
626
+ let patched;
627
+ const loaded = await this.getById(id, opt);
628
+ if (loaded) {
629
+ patched = { ...loaded, ...patch };
630
+ if ((0, js_lib_1._deepJsonEquals)(loaded, patched)) {
631
+ // Skipping the save operation, as data is the same
632
+ return patched;
633
+ }
634
+ }
635
+ else {
636
+ patched = this.create({ ...patch, id }, opt);
637
+ }
638
+ return await this.save(patched, opt);
639
+ }
640
+ /**
641
+ * Same as patchById, but takes the whole object as input.
642
+ * This "whole object" is mutated with the patch and returned.
643
+ * Otherwise, similar behavior as patchById.
644
+ * It still loads the row from the DB.
645
+ */
646
+ async patch(bm, patch, opt = {}) {
647
+ (0, js_lib_1._assert)(bm.id, 'patch argument object should have an id', {
648
+ bm,
649
+ });
650
+ const loaded = await this.getById(bm.id, opt);
651
+ if (loaded) {
652
+ Object.assign(loaded, patch);
653
+ if ((0, js_lib_1._deepJsonEquals)(loaded, bm)) {
654
+ // Skipping the save operation, as data is the same
655
+ return bm;
656
+ }
657
+ // Make `bm` exactly the same as `loaded`
658
+ (0, js_lib_1._objectAssignExact)(bm, loaded);
659
+ }
660
+ else {
661
+ Object.assign(bm, patch);
662
+ }
663
+ return await this.save(bm, opt);
612
664
  }
613
665
  async saveAsDBM(dbm, opt = {}) {
614
666
  this.requireWriteAccess();
@@ -237,10 +237,21 @@ export interface CommonDaoOptions extends CommonDBOptions {
237
237
  */
238
238
  tx?: boolean;
239
239
  }
240
+ export interface CommonDaoSaveOptions<BM extends Partial<ObjectWithId>, DBM extends ObjectWithId> extends CommonDaoSaveBatchOptions<DBM> {
241
+ /**
242
+ * If provided - a check will be made.
243
+ * If the object for saving equals to the object passed to `skipIfEquals` - save operation will be skipped.
244
+ *
245
+ * Equality is checked with _deepJsonEquals (aka "deep equals after JSON.stringify/parse", which removes keys with undefined values).
246
+ *
247
+ * It's supposed to be used to prevent "unnecessary saves", when data is not changed.
248
+ */
249
+ skipIfEquals?: BM;
250
+ }
240
251
  /**
241
252
  * All properties default to undefined.
242
253
  */
243
- export interface CommonDaoSaveOptions<DBM extends ObjectWithId> extends CommonDaoOptions, CommonDBSaveOptions<DBM> {
254
+ export interface CommonDaoSaveBatchOptions<DBM extends ObjectWithId> extends CommonDaoOptions, CommonDBSaveOptions<DBM> {
244
255
  /**
245
256
  * @default false
246
257
  *
@@ -254,7 +265,7 @@ export interface CommonDaoSaveOptions<DBM extends ObjectWithId> extends CommonDa
254
265
  }
255
266
  export interface CommonDaoStreamDeleteOptions<DBM extends ObjectWithId> extends CommonDaoStreamOptions<DBM> {
256
267
  }
257
- export interface CommonDaoStreamSaveOptions<DBM extends ObjectWithId> extends CommonDaoSaveOptions<DBM>, CommonDaoStreamOptions<DBM> {
268
+ export interface CommonDaoStreamSaveOptions<DBM extends ObjectWithId> extends CommonDaoSaveBatchOptions<DBM>, CommonDaoStreamOptions<DBM> {
258
269
  }
259
270
  export interface CommonDaoStreamForEachOptions<IN> extends CommonDaoStreamOptions<IN>, TransformMapOptions<IN, any> {
260
271
  }
package/package.json CHANGED
@@ -40,7 +40,7 @@
40
40
  "engines": {
41
41
  "node": ">=18.12"
42
42
  },
43
- "version": "8.55.1",
43
+ "version": "8.57.0",
44
44
  "description": "Lowest Common Denominator API to supported Databases",
45
45
  "keywords": [
46
46
  "db",
@@ -298,10 +298,23 @@ export interface CommonDaoOptions extends CommonDBOptions {
298
298
  tx?: boolean
299
299
  }
300
300
 
301
+ export interface CommonDaoSaveOptions<BM extends Partial<ObjectWithId>, DBM extends ObjectWithId>
302
+ extends CommonDaoSaveBatchOptions<DBM> {
303
+ /**
304
+ * If provided - a check will be made.
305
+ * If the object for saving equals to the object passed to `skipIfEquals` - save operation will be skipped.
306
+ *
307
+ * Equality is checked with _deepJsonEquals (aka "deep equals after JSON.stringify/parse", which removes keys with undefined values).
308
+ *
309
+ * It's supposed to be used to prevent "unnecessary saves", when data is not changed.
310
+ */
311
+ skipIfEquals?: BM
312
+ }
313
+
301
314
  /**
302
315
  * All properties default to undefined.
303
316
  */
304
- export interface CommonDaoSaveOptions<DBM extends ObjectWithId>
317
+ export interface CommonDaoSaveBatchOptions<DBM extends ObjectWithId>
305
318
  extends CommonDaoOptions,
306
319
  CommonDBSaveOptions<DBM> {
307
320
  /**
@@ -320,7 +333,7 @@ export interface CommonDaoStreamDeleteOptions<DBM extends ObjectWithId>
320
333
  extends CommonDaoStreamOptions<DBM> {}
321
334
 
322
335
  export interface CommonDaoStreamSaveOptions<DBM extends ObjectWithId>
323
- extends CommonDaoSaveOptions<DBM>,
336
+ extends CommonDaoSaveBatchOptions<DBM>,
324
337
  CommonDaoStreamOptions<DBM> {}
325
338
 
326
339
  export interface CommonDaoStreamForEachOptions<IN>
@@ -1,9 +1,11 @@
1
1
  import { Transform } from 'node:stream'
2
2
  import {
3
3
  _assert,
4
+ _deepJsonEquals,
4
5
  _filterNullishValues,
5
6
  _filterUndefinedValues,
6
7
  _isTruthy,
8
+ _objectAssignExact,
7
9
  _passthroughPredicate,
8
10
  _since,
9
11
  _truncate,
@@ -57,6 +59,7 @@ import {
57
59
  CommonDaoHooks,
58
60
  CommonDaoLogLevel,
59
61
  CommonDaoOptions,
62
+ CommonDaoSaveBatchOptions,
60
63
  CommonDaoSaveOptions,
61
64
  CommonDaoStreamDeleteOptions,
62
65
  CommonDaoStreamForEachOptions,
@@ -697,7 +700,7 @@ export class CommonDao<
697
700
  tx = {
698
701
  save: async (
699
702
  bm: Unsaved<BM>,
700
- opt: CommonDaoSaveOptions<DBM> = {},
703
+ opt: CommonDaoSaveBatchOptions<DBM> = {},
701
704
  ): Promise<DBSaveBatchOperation | undefined> => {
702
705
  // .save actually returns DBM (not BM) when it detects `opt.tx === true`
703
706
  const row: DBM | null = (await this.save(bm, { ...opt, tx: true })) as any
@@ -715,7 +718,7 @@ export class CommonDao<
715
718
  },
716
719
  saveBatch: async (
717
720
  bms: Unsaved<BM>[],
718
- opt: CommonDaoSaveOptions<DBM> = {},
721
+ opt: CommonDaoSaveBatchOptions<DBM> = {},
719
722
  ): Promise<DBSaveBatchOperation | undefined> => {
720
723
  const rows: DBM[] = (await this.saveBatch(bms, { ...opt, tx: true })) as any
721
724
  if (!rows.length) return
@@ -760,8 +763,14 @@ export class CommonDao<
760
763
  /**
761
764
  * Mutates with id, created, updated
762
765
  */
763
- async save(bm: Unsaved<BM>, opt: CommonDaoSaveOptions<DBM> = {}): Promise<Saved<BM>> {
766
+ async save(bm: Unsaved<BM>, opt: CommonDaoSaveOptions<BM, DBM> = {}): Promise<Saved<BM>> {
764
767
  this.requireWriteAccess()
768
+
769
+ if (opt.skipIfEquals && _deepJsonEquals(bm, opt.skipIfEquals)) {
770
+ // Skipping the save operation
771
+ return bm as Saved<BM>
772
+ }
773
+
765
774
  const idWasGenerated = !bm.id && this.cfg.createId
766
775
  this.assignIdCreatedUpdated(bm, opt) // mutates
767
776
  let dbm = await this.bmToDBM(bm as BM, opt)
@@ -801,38 +810,100 @@ export class CommonDao<
801
810
  }
802
811
 
803
812
  /**
804
- * Loads the row by id.
805
- * Creates the row (via this.create()) if it doesn't exist
806
- * (this will cause a validation error if Patch has not enough data for the row to be valid).
807
- * Saves (as fast as possible) with the Patch applied.
813
+ * 1. Applies the patch
814
+ * 2. If object is the same after patching - skips saving it
815
+ * 3. Otherwise - saves the patched object and returns it
808
816
  *
809
- * Convenience method to replace 3 operations (loading+patching+saving) with one.
817
+ * Similar to `save` with skipIfEquals.
818
+ * Similar to `patch`, but doesn't load the object from the Database.
810
819
  */
811
- async patch(id: ID, patch: Partial<BM>, opt: CommonDaoSaveOptions<DBM> = {}): Promise<Saved<BM>> {
812
- return await this.save(
813
- {
814
- ...(await this.getByIdOrEmpty(id, patch, opt)),
815
- ...patch,
816
- } as any,
817
- opt,
818
- )
820
+ async savePatch(
821
+ bm: Saved<BM>,
822
+ patch: Partial<BM>,
823
+ opt: CommonDaoSaveBatchOptions<DBM>,
824
+ ): Promise<Saved<BM>> {
825
+ const patched: Saved<BM> = {
826
+ ...bm,
827
+ ...patch,
828
+ }
829
+
830
+ if (_deepJsonEquals(bm, patched)) {
831
+ // Skipping the save operation, as data is the same
832
+ return bm
833
+ }
834
+
835
+ // Actually apply the patch by mutating the original object (by design)
836
+ Object.assign(bm, patch)
837
+
838
+ return await this.save(bm, opt)
819
839
  }
820
840
 
821
- async patchAsDBM(id: ID, patch: Partial<DBM>, opt: CommonDaoSaveOptions<DBM> = {}): Promise<DBM> {
822
- const dbm =
823
- (await this.getByIdAsDBM(id, opt)) ||
824
- (this.create({ ...patch, id } as Partial<BM>, opt) as any as DBM)
841
+ /**
842
+ * Convenience method to replace 3 operations (loading+patching+saving) with one:
843
+ *
844
+ * 1. Loads the row by id.
845
+ * 1.1 Creates the row (via this.create()) if it doesn't exist
846
+ * (this will cause a validation error if Patch has not enough data for the row to be valid).
847
+ * 2. Applies the patch on top of loaded data.
848
+ * 3. Saves (as fast as possible since the read) with the Patch applied, but only if the data has changed.
849
+ */
850
+ async patchById(
851
+ id: ID,
852
+ patch: Partial<BM>,
853
+ opt: CommonDaoSaveBatchOptions<DBM> = {},
854
+ ): Promise<Saved<BM>> {
855
+ let patched: Saved<BM>
856
+ const loaded = await this.getById(id, opt)
857
+
858
+ if (loaded) {
859
+ patched = { ...loaded, ...patch }
860
+
861
+ if (_deepJsonEquals(loaded, patched)) {
862
+ // Skipping the save operation, as data is the same
863
+ return patched
864
+ }
865
+ } else {
866
+ patched = this.create({ ...patch, id }, opt)
867
+ }
825
868
 
826
- return await this.saveAsDBM(
827
- {
828
- ...dbm,
829
- ...patch,
830
- },
831
- opt,
832
- )
869
+ return await this.save(patched, opt)
870
+ }
871
+
872
+ /**
873
+ * Same as patchById, but takes the whole object as input.
874
+ * This "whole object" is mutated with the patch and returned.
875
+ * Otherwise, similar behavior as patchById.
876
+ * It still loads the row from the DB.
877
+ */
878
+ async patch(
879
+ bm: Saved<BM>,
880
+ patch: Partial<BM>,
881
+ opt: CommonDaoSaveBatchOptions<DBM> = {},
882
+ ): Promise<Saved<BM>> {
883
+ _assert(bm.id, 'patch argument object should have an id', {
884
+ bm,
885
+ })
886
+
887
+ const loaded = await this.getById(bm.id, opt)
888
+
889
+ if (loaded) {
890
+ Object.assign(loaded, patch)
891
+
892
+ if (_deepJsonEquals(loaded, bm)) {
893
+ // Skipping the save operation, as data is the same
894
+ return bm
895
+ }
896
+
897
+ // Make `bm` exactly the same as `loaded`
898
+ _objectAssignExact(bm, loaded)
899
+ } else {
900
+ Object.assign(bm, patch)
901
+ }
902
+
903
+ return await this.save(bm, opt)
833
904
  }
834
905
 
835
- async saveAsDBM(dbm: DBM, opt: CommonDaoSaveOptions<DBM> = {}): Promise<DBM> {
906
+ async saveAsDBM(dbm: DBM, opt: CommonDaoSaveBatchOptions<DBM> = {}): Promise<DBM> {
836
907
  this.requireWriteAccess()
837
908
  const table = opt.table || this.cfg.table
838
909
 
@@ -872,7 +943,10 @@ export class CommonDao<
872
943
  return row
873
944
  }
874
945
 
875
- async saveBatch(bms: Unsaved<BM>[], opt: CommonDaoSaveOptions<DBM> = {}): Promise<Saved<BM>[]> {
946
+ async saveBatch(
947
+ bms: Unsaved<BM>[],
948
+ opt: CommonDaoSaveBatchOptions<DBM> = {},
949
+ ): Promise<Saved<BM>[]> {
876
950
  if (!bms.length) return []
877
951
  this.requireWriteAccess()
878
952
  const table = opt.table || this.cfg.table
@@ -920,7 +994,7 @@ export class CommonDao<
920
994
  return bms as any[]
921
995
  }
922
996
 
923
- async saveBatchAsDBM(dbms: DBM[], opt: CommonDaoSaveOptions<DBM> = {}): Promise<DBM[]> {
997
+ async saveBatchAsDBM(dbms: DBM[], opt: CommonDaoSaveBatchOptions<DBM> = {}): Promise<DBM[]> {
924
998
  if (!dbms.length) return []
925
999
  this.requireWriteAccess()
926
1000
  const table = opt.table || this.cfg.table