@naturalcycles/db-lib 8.56.0 → 8.58.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.
@@ -111,10 +111,16 @@ export declare class CommonDao<BM extends Partial<ObjectWithId<ID>>, DBM extends
111
111
  * 1.1 Creates the row (via this.create()) if it doesn't exist
112
112
  * (this will cause a validation error if Patch has not enough data for the row to be valid).
113
113
  * 2. Applies the patch on top of loaded data.
114
- * 3. Saves (as fast as possible since the read) with the Patch applied.
114
+ * 3. Saves (as fast as possible since the read) with the Patch applied, but only if the data has changed.
115
115
  */
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>;
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.
122
+ */
123
+ patch(bm: Saved<BM>, patch: Partial<BM>, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>>;
118
124
  saveAsDBM(dbm: DBM, opt?: CommonDaoSaveBatchOptions<DBM>): Promise<DBM>;
119
125
  saveBatch(bms: Unsaved<BM>[], opt?: CommonDaoSaveBatchOptions<DBM>): Promise<Saved<BM>[]>;
120
126
  saveBatchAsDBM(dbms: DBM[], opt?: CommonDaoSaveBatchOptions<DBM>): Promise<DBM[]>;
@@ -620,19 +620,16 @@ class CommonDao {
620
620
  * 1.1 Creates the row (via this.create()) if it doesn't exist
621
621
  * (this will cause a validation error if Patch has not enough data for the row to be valid).
622
622
  * 2. Applies the patch on top of loaded data.
623
- * 3. Saves (as fast as possible since the read) with the Patch applied.
623
+ * 3. Saves (as fast as possible since the read) with the Patch applied, but only if the data has changed.
624
624
  */
625
- async patch(id, patch, opt = {}) {
626
- const bm = await this.getById(id, opt);
625
+ async patchById(id, patch, opt = {}) {
627
626
  let patched;
628
- if (bm) {
629
- patched = {
630
- ...bm,
631
- ...patch,
632
- };
633
- if ((0, js_lib_1._deepJsonEquals)(bm, 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)) {
634
631
  // Skipping the save operation, as data is the same
635
- return bm;
632
+ return patched;
636
633
  }
637
634
  }
638
635
  else {
@@ -640,23 +637,30 @@ class CommonDao {
640
637
  }
641
638
  return await this.save(patched, opt);
642
639
  }
643
- async patchAsDBM(id, patch, 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)) {
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)) {
652
654
  // Skipping the save operation, as data is the same
653
- return dbm;
655
+ return bm;
654
656
  }
657
+ // Make `bm` exactly the same as `loaded`
658
+ (0, js_lib_1._objectAssignExact)(bm, loaded);
655
659
  }
656
660
  else {
657
- patched = this.create({ ...patch, id }, opt);
661
+ Object.assign(bm, patch);
658
662
  }
659
- return await this.saveAsDBM(patched, opt);
663
+ return await this.save(bm, opt);
660
664
  }
661
665
  async saveAsDBM(dbm, opt = {}) {
662
666
  this.requireWriteAccess();
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  import { ZlibOptions } from 'node:zlib';
3
- import { AsyncMapper, ErrorMode } from '@naturalcycles/js-lib';
3
+ import { AsyncMapper, ErrorMode, UnixTimestampNumber, StringMap } from '@naturalcycles/js-lib';
4
4
  import { NDJsonStats, TransformLogProgressOptions, TransformMapOptions } from '@naturalcycles/nodejs-lib';
5
5
  import { CommonDB } from '../common.db';
6
6
  export interface DBPipelineBackupOptions extends TransformLogProgressOptions {
@@ -33,10 +33,13 @@ export interface DBPipelineBackupOptions extends TransformLogProgressOptions {
33
33
  limit?: number;
34
34
  /**
35
35
  * If set - will do "incremental backup" (not full), only for entities that updated >= `sinceUpdated`
36
- *
37
- * @default undefined
38
36
  */
39
- sinceUpdated?: number;
37
+ sinceUpdated?: UnixTimestampNumber;
38
+ /**
39
+ * Map for each table a `sinceUpdated` timestamp, or `undefined`.
40
+ * If set - will do "incremental backup" (not full), only for entities that updated >= `sinceUpdated` (on a per table basis)
41
+ */
42
+ sinceUpdatedPerTable?: StringMap<UnixTimestampNumber>;
40
43
  /**
41
44
  * Directory path to store dumped files. Will create `${tableName}.ndjson` (or .ndjson.gz if gzip=true) files.
42
45
  * All parent directories will be created.
@@ -63,7 +66,7 @@ export interface DBPipelineBackupOptions extends TransformLogProgressOptions {
63
66
  * @default `{}`
64
67
  * Default mappers will be "passthroughMapper" (pass all data as-is).
65
68
  */
66
- mapperPerTable?: Record<string, AsyncMapper>;
69
+ mapperPerTable?: StringMap<AsyncMapper>;
67
70
  /**
68
71
  * You can alter default `transformMapOptions` here.
69
72
  *
@@ -18,17 +18,21 @@ const index_1 = require("../index");
18
18
  * Optionally you can provide mapperPerTable and @param transformMapOptions (one for all mappers) - it will run for each table.
19
19
  */
20
20
  async function dbPipelineBackup(opt) {
21
- const { db, concurrency = 16, limit = 0, sinceUpdated, outputDirPath, protectFromOverwrite = false, zlibOptions, mapperPerTable = {}, transformMapOptions, errorMode = js_lib_1.ErrorMode.SUPPRESS, emitSchemaFromDB = false, sortObjects = false, } = opt;
21
+ const { db, concurrency = 16, limit = 0, outputDirPath, protectFromOverwrite = false, zlibOptions, mapperPerTable = {}, transformMapOptions, errorMode = js_lib_1.ErrorMode.SUPPRESS, emitSchemaFromDB = false, sortObjects = false, } = opt;
22
22
  const strict = errorMode !== js_lib_1.ErrorMode.SUPPRESS;
23
23
  const gzip = opt.gzip !== false; // default to true
24
24
  let { tables } = opt;
25
- const sinceUpdatedStr = sinceUpdated ? ' since ' + (0, nodejs_lib_1.grey)((0, js_lib_1.localTime)(sinceUpdated).toPretty()) : '';
26
- console.log(`>> ${(0, nodejs_lib_1.dimWhite)('dbPipelineBackup')} started in ${(0, nodejs_lib_1.grey)(outputDirPath)}...${sinceUpdatedStr}`);
25
+ console.log(`>> ${(0, nodejs_lib_1.dimWhite)('dbPipelineBackup')} started in ${(0, nodejs_lib_1.grey)(outputDirPath)}...`);
27
26
  (0, nodejs_lib_1._ensureDirSync)(outputDirPath);
28
27
  tables ||= await db.getTables();
29
28
  console.log(`${(0, nodejs_lib_1.yellow)(tables.length)} ${(0, nodejs_lib_1.boldWhite)('table(s)')}:\n` + tables.join('\n'));
30
29
  const statsPerTable = {};
31
30
  await (0, js_lib_1.pMap)(tables, async (table) => {
31
+ const sinceUpdated = opt.sinceUpdatedPerTable?.[table] || opt.sinceUpdated;
32
+ const sinceUpdatedStr = sinceUpdated
33
+ ? ' since ' + (0, nodejs_lib_1.grey)((0, js_lib_1.localTime)(sinceUpdated).toPretty())
34
+ : '';
35
+ console.log(`>> ${(0, nodejs_lib_1.grey)(table)}${sinceUpdatedStr}`);
32
36
  let q = index_1.DBQuery.create(table).limit(limit);
33
37
  if (sinceUpdated) {
34
38
  q = q.filter('updated', '>=', sinceUpdated);
package/package.json CHANGED
@@ -40,7 +40,7 @@
40
40
  "engines": {
41
41
  "node": ">=18.12"
42
42
  },
43
- "version": "8.56.0",
43
+ "version": "8.58.0",
44
44
  "description": "Lowest Common Denominator API to supported Databases",
45
45
  "keywords": [
46
46
  "db",
@@ -5,6 +5,7 @@ import {
5
5
  _filterNullishValues,
6
6
  _filterUndefinedValues,
7
7
  _isTruthy,
8
+ _objectAssignExact,
8
9
  _passthroughPredicate,
9
10
  _since,
10
11
  _truncate,
@@ -844,25 +845,22 @@ export class CommonDao<
844
845
  * 1.1 Creates the row (via this.create()) if it doesn't exist
845
846
  * (this will cause a validation error if Patch has not enough data for the row to be valid).
846
847
  * 2. Applies the patch on top of loaded data.
847
- * 3. Saves (as fast as possible since the read) with the Patch applied.
848
+ * 3. Saves (as fast as possible since the read) with the Patch applied, but only if the data has changed.
848
849
  */
849
- async patch(
850
+ async patchById(
850
851
  id: ID,
851
852
  patch: Partial<BM>,
852
853
  opt: CommonDaoSaveBatchOptions<DBM> = {},
853
854
  ): Promise<Saved<BM>> {
854
- const bm = await this.getById(id, opt)
855
855
  let patched: Saved<BM>
856
+ const loaded = await this.getById(id, opt)
856
857
 
857
- if (bm) {
858
- patched = {
859
- ...bm,
860
- ...patch,
861
- }
858
+ if (loaded) {
859
+ patched = { ...loaded, ...patch }
862
860
 
863
- if (_deepJsonEquals(bm, patched)) {
861
+ if (_deepJsonEquals(loaded, patched)) {
864
862
  // Skipping the save operation, as data is the same
865
- return bm
863
+ return patched
866
864
  }
867
865
  } else {
868
866
  patched = this.create({ ...patch, id }, opt)
@@ -871,29 +869,38 @@ export class CommonDao<
871
869
  return await this.save(patched, opt)
872
870
  }
873
871
 
874
- async patchAsDBM(
875
- id: ID,
876
- patch: Partial<DBM>,
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>,
877
881
  opt: CommonDaoSaveBatchOptions<DBM> = {},
878
- ): Promise<DBM> {
879
- const dbm = await this.getByIdAsDBM(id, opt)
880
- let patched: DBM
882
+ ): Promise<Saved<BM>> {
883
+ _assert(bm.id, 'patch argument object should have an id', {
884
+ bm,
885
+ })
881
886
 
882
- if (dbm) {
883
- patched = {
884
- ...dbm,
885
- ...patch,
886
- }
887
+ const loaded = await this.getById(bm.id, opt)
887
888
 
888
- if (_deepJsonEquals(dbm, patched)) {
889
+ if (loaded) {
890
+ Object.assign(loaded, patch)
891
+
892
+ if (_deepJsonEquals(loaded, bm)) {
889
893
  // Skipping the save operation, as data is the same
890
- return dbm
894
+ return bm
891
895
  }
896
+
897
+ // Make `bm` exactly the same as `loaded`
898
+ _objectAssignExact(bm, loaded)
892
899
  } else {
893
- patched = this.create({ ...patch, id } as Partial<BM>, opt) as any as DBM
900
+ Object.assign(bm, patch)
894
901
  }
895
902
 
896
- return await this.saveAsDBM(patched, opt)
903
+ return await this.save(bm, opt)
897
904
  }
898
905
 
899
906
  async saveAsDBM(dbm: DBM, opt: CommonDaoSaveBatchOptions<DBM> = {}): Promise<DBM> {
@@ -8,6 +8,8 @@ import {
8
8
  pMap,
9
9
  _passthroughMapper,
10
10
  localTime,
11
+ UnixTimestampNumber,
12
+ StringMap,
11
13
  } from '@naturalcycles/js-lib'
12
14
  import {
13
15
  NDJsonStats,
@@ -65,10 +67,14 @@ export interface DBPipelineBackupOptions extends TransformLogProgressOptions {
65
67
 
66
68
  /**
67
69
  * If set - will do "incremental backup" (not full), only for entities that updated >= `sinceUpdated`
68
- *
69
- * @default undefined
70
70
  */
71
- sinceUpdated?: number
71
+ sinceUpdated?: UnixTimestampNumber
72
+
73
+ /**
74
+ * Map for each table a `sinceUpdated` timestamp, or `undefined`.
75
+ * If set - will do "incremental backup" (not full), only for entities that updated >= `sinceUpdated` (on a per table basis)
76
+ */
77
+ sinceUpdatedPerTable?: StringMap<UnixTimestampNumber>
72
78
 
73
79
  /**
74
80
  * Directory path to store dumped files. Will create `${tableName}.ndjson` (or .ndjson.gz if gzip=true) files.
@@ -100,7 +106,7 @@ export interface DBPipelineBackupOptions extends TransformLogProgressOptions {
100
106
  * @default `{}`
101
107
  * Default mappers will be "passthroughMapper" (pass all data as-is).
102
108
  */
103
- mapperPerTable?: Record<string, AsyncMapper>
109
+ mapperPerTable?: StringMap<AsyncMapper>
104
110
 
105
111
  /**
106
112
  * You can alter default `transformMapOptions` here.
@@ -143,7 +149,6 @@ export async function dbPipelineBackup(opt: DBPipelineBackupOptions): Promise<ND
143
149
  db,
144
150
  concurrency = 16,
145
151
  limit = 0,
146
- sinceUpdated,
147
152
  outputDirPath,
148
153
  protectFromOverwrite = false,
149
154
  zlibOptions,
@@ -158,11 +163,7 @@ export async function dbPipelineBackup(opt: DBPipelineBackupOptions): Promise<ND
158
163
 
159
164
  let { tables } = opt
160
165
 
161
- const sinceUpdatedStr = sinceUpdated ? ' since ' + grey(localTime(sinceUpdated).toPretty()) : ''
162
-
163
- console.log(
164
- `>> ${dimWhite('dbPipelineBackup')} started in ${grey(outputDirPath)}...${sinceUpdatedStr}`,
165
- )
166
+ console.log(`>> ${dimWhite('dbPipelineBackup')} started in ${grey(outputDirPath)}...`)
166
167
 
167
168
  _ensureDirSync(outputDirPath)
168
169
 
@@ -175,6 +176,14 @@ export async function dbPipelineBackup(opt: DBPipelineBackupOptions): Promise<ND
175
176
  await pMap(
176
177
  tables,
177
178
  async table => {
179
+ const sinceUpdated = opt.sinceUpdatedPerTable?.[table] || opt.sinceUpdated
180
+
181
+ const sinceUpdatedStr = sinceUpdated
182
+ ? ' since ' + grey(localTime(sinceUpdated).toPretty())
183
+ : ''
184
+
185
+ console.log(`>> ${grey(table)}${sinceUpdatedStr}`)
186
+
178
187
  let q = DBQuery.create(table).limit(limit)
179
188
 
180
189
  if (sinceUpdated) {