@naturalcycles/db-lib 8.55.1 → 8.56.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,38 @@ 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
103
102
  *
104
- * Convenience method to replace 3 operations (loading+patching+saving) with one.
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:
109
+ *
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.
105
115
  */
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[]>;
116
+ patch(id: ID, patch: Partial<BM>, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>>;
117
+ patchAsDBM(id: ID, patch: Partial<DBM>, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<DBM>;
118
+ saveAsDBM(dbm: DBM, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<DBM>;
119
+ saveBatch(bms: Unsaved<BM>[], opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>[]>;
120
+ saveBatchAsDBM(dbms: DBM[], opt?: CommonDaoSaveBatchOptions<DBM>): Promise<DBM[]>;
111
121
  /**
112
122
  * "Streaming" is implemented by buffering incoming rows into **batches**
113
123
  * (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,70 @@ 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);
615
+ }
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.
624
+ */
625
+ async patch(id, patch, opt = {}) {
626
+ const bm = await this.getById(id, opt);
627
+ let patched;
628
+ if (bm) {
629
+ patched = {
630
+ ...bm,
631
+ ...patch,
632
+ };
633
+ if ((0, js_lib_1._deepJsonEquals)(bm, patched)) {
634
+ // Skipping the save operation, as data is the same
635
+ return bm;
636
+ }
637
+ }
638
+ else {
639
+ patched = this.create({ ...patch, id }, opt);
640
+ }
641
+ return await this.save(patched, opt);
604
642
  }
605
643
  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);
644
+ const dbm = await this.getByIdAsDBM(id, opt);
645
+ let patched;
646
+ if (dbm) {
647
+ patched = {
648
+ ...dbm,
649
+ ...patch,
650
+ };
651
+ if ((0, js_lib_1._deepJsonEquals)(dbm, patched)) {
652
+ // Skipping the save operation, as data is the same
653
+ return dbm;
654
+ }
655
+ }
656
+ else {
657
+ patched = this.create({ ...patch, id }, opt);
658
+ }
659
+ return await this.saveAsDBM(patched, opt);
612
660
  }
613
661
  async saveAsDBM(dbm, opt = {}) {
614
662
  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.56.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,6 +1,7 @@
1
1
  import { Transform } from 'node:stream'
2
2
  import {
3
3
  _assert,
4
+ _deepJsonEquals,
4
5
  _filterNullishValues,
5
6
  _filterUndefinedValues,
6
7
  _isTruthy,
@@ -57,6 +58,7 @@ import {
57
58
  CommonDaoHooks,
58
59
  CommonDaoLogLevel,
59
60
  CommonDaoOptions,
61
+ CommonDaoSaveBatchOptions,
60
62
  CommonDaoSaveOptions,
61
63
  CommonDaoStreamDeleteOptions,
62
64
  CommonDaoStreamForEachOptions,
@@ -697,7 +699,7 @@ export class CommonDao<
697
699
  tx = {
698
700
  save: async (
699
701
  bm: Unsaved<BM>,
700
- opt: CommonDaoSaveOptions<DBM> = {},
702
+ opt: CommonDaoSaveBatchOptions<DBM> = {},
701
703
  ): Promise<DBSaveBatchOperation | undefined> => {
702
704
  // .save actually returns DBM (not BM) when it detects `opt.tx === true`
703
705
  const row: DBM | null = (await this.save(bm, { ...opt, tx: true })) as any
@@ -715,7 +717,7 @@ export class CommonDao<
715
717
  },
716
718
  saveBatch: async (
717
719
  bms: Unsaved<BM>[],
718
- opt: CommonDaoSaveOptions<DBM> = {},
720
+ opt: CommonDaoSaveBatchOptions<DBM> = {},
719
721
  ): Promise<DBSaveBatchOperation | undefined> => {
720
722
  const rows: DBM[] = (await this.saveBatch(bms, { ...opt, tx: true })) as any
721
723
  if (!rows.length) return
@@ -760,8 +762,14 @@ export class CommonDao<
760
762
  /**
761
763
  * Mutates with id, created, updated
762
764
  */
763
- async save(bm: Unsaved<BM>, opt: CommonDaoSaveOptions<DBM> = {}): Promise<Saved<BM>> {
765
+ async save(bm: Unsaved<BM>, opt: CommonDaoSaveOptions<BM, DBM> = {}): Promise<Saved<BM>> {
764
766
  this.requireWriteAccess()
767
+
768
+ if (opt.skipIfEquals && _deepJsonEquals(bm, opt.skipIfEquals)) {
769
+ // Skipping the save operation
770
+ return bm as Saved<BM>
771
+ }
772
+
765
773
  const idWasGenerated = !bm.id && this.cfg.createId
766
774
  this.assignIdCreatedUpdated(bm, opt) // mutates
767
775
  let dbm = await this.bmToDBM(bm as BM, opt)
@@ -801,38 +809,94 @@ export class CommonDao<
801
809
  }
802
810
 
803
811
  /**
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.
812
+ * 1. Applies the patch
813
+ * 2. If object is the same after patching - skips saving it
814
+ * 3. Otherwise - saves the patched object and returns it
815
+ *
816
+ * Similar to `save` with skipIfEquals.
817
+ * Similar to `patch`, but doesn't load the object from the Database.
818
+ */
819
+ async savePatch(
820
+ bm: Saved<BM>,
821
+ patch: Partial<BM>,
822
+ opt: CommonDaoSaveBatchOptions<DBM>,
823
+ ): Promise<Saved<BM>> {
824
+ const patched: Saved<BM> = {
825
+ ...bm,
826
+ ...patch,
827
+ }
828
+
829
+ if (_deepJsonEquals(bm, patched)) {
830
+ // Skipping the save operation, as data is the same
831
+ return bm
832
+ }
833
+
834
+ // Actually apply the patch by mutating the original object (by design)
835
+ Object.assign(bm, patch)
836
+
837
+ return await this.save(bm, opt)
838
+ }
839
+
840
+ /**
841
+ * Convenience method to replace 3 operations (loading+patching+saving) with one:
808
842
  *
809
- * Convenience method to replace 3 operations (loading+patching+saving) with one.
843
+ * 1. Loads the row by id.
844
+ * 1.1 Creates the row (via this.create()) if it doesn't exist
845
+ * (this will cause a validation error if Patch has not enough data for the row to be valid).
846
+ * 2. Applies the patch on top of loaded data.
847
+ * 3. Saves (as fast as possible since the read) with the Patch applied.
810
848
  */
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)),
849
+ async patch(
850
+ id: ID,
851
+ patch: Partial<BM>,
852
+ opt: CommonDaoSaveBatchOptions<DBM> = {},
853
+ ): Promise<Saved<BM>> {
854
+ const bm = await this.getById(id, opt)
855
+ let patched: Saved<BM>
856
+
857
+ if (bm) {
858
+ patched = {
859
+ ...bm,
815
860
  ...patch,
816
- } as any,
817
- opt,
818
- )
861
+ }
862
+
863
+ if (_deepJsonEquals(bm, patched)) {
864
+ // Skipping the save operation, as data is the same
865
+ return bm
866
+ }
867
+ } else {
868
+ patched = this.create({ ...patch, id }, opt)
869
+ }
870
+
871
+ return await this.save(patched, opt)
819
872
  }
820
873
 
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)
874
+ async patchAsDBM(
875
+ id: ID,
876
+ patch: Partial<DBM>,
877
+ opt: CommonDaoSaveBatchOptions<DBM> = {},
878
+ ): Promise<DBM> {
879
+ const dbm = await this.getByIdAsDBM(id, opt)
880
+ let patched: DBM
825
881
 
826
- return await this.saveAsDBM(
827
- {
882
+ if (dbm) {
883
+ patched = {
828
884
  ...dbm,
829
885
  ...patch,
830
- },
831
- opt,
832
- )
886
+ }
887
+
888
+ if (_deepJsonEquals(dbm, patched)) {
889
+ // Skipping the save operation, as data is the same
890
+ return dbm
891
+ }
892
+ } else {
893
+ patched = this.create({ ...patch, id } as Partial<BM>, opt) as any as DBM
894
+ }
895
+
896
+ return await this.saveAsDBM(patched, opt)
833
897
  }
834
898
 
835
- async saveAsDBM(dbm: DBM, opt: CommonDaoSaveOptions<DBM> = {}): Promise<DBM> {
899
+ async saveAsDBM(dbm: DBM, opt: CommonDaoSaveBatchOptions<DBM> = {}): Promise<DBM> {
836
900
  this.requireWriteAccess()
837
901
  const table = opt.table || this.cfg.table
838
902
 
@@ -872,7 +936,10 @@ export class CommonDao<
872
936
  return row
873
937
  }
874
938
 
875
- async saveBatch(bms: Unsaved<BM>[], opt: CommonDaoSaveOptions<DBM> = {}): Promise<Saved<BM>[]> {
939
+ async saveBatch(
940
+ bms: Unsaved<BM>[],
941
+ opt: CommonDaoSaveBatchOptions<DBM> = {},
942
+ ): Promise<Saved<BM>[]> {
876
943
  if (!bms.length) return []
877
944
  this.requireWriteAccess()
878
945
  const table = opt.table || this.cfg.table
@@ -920,7 +987,7 @@ export class CommonDao<
920
987
  return bms as any[]
921
988
  }
922
989
 
923
- async saveBatchAsDBM(dbms: DBM[], opt: CommonDaoSaveOptions<DBM> = {}): Promise<DBM[]> {
990
+ async saveBatchAsDBM(dbms: DBM[], opt: CommonDaoSaveBatchOptions<DBM> = {}): Promise<DBM[]> {
924
991
  if (!dbms.length) return []
925
992
  this.requireWriteAccess()
926
993
  const table = opt.table || this.cfg.table