@naturalcycles/db-lib 10.40.1 → 10.41.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.
@@ -131,9 +131,17 @@ export declare class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity
131
131
  bmToDBM(bm: undefined, opt?: CommonDaoOptions): Promise<null>;
132
132
  bmToDBM(bm?: BM, opt?: CommonDaoOptions): Promise<DBM>;
133
133
  bmsToDBM(bms: BM[], opt?: CommonDaoOptions): Promise<DBM[]>;
134
- anyToDBM(dbm: undefined, opt?: CommonDaoOptions): null;
135
- anyToDBM(dbm?: any, opt?: CommonDaoOptions): DBM;
136
- anyToDBMs(rows: DBM[], opt?: CommonDaoOptions): DBM[];
134
+ /**
135
+ * Mutates `dbm`.
136
+ */
137
+ compress(dbm: DBM): Promise<void>;
138
+ /**
139
+ * Mutates `dbm`.
140
+ */
141
+ decompress(dbm: DBM): Promise<void>;
142
+ anyToDBM(dbm: undefined, opt?: CommonDaoOptions): Promise<null>;
143
+ anyToDBM(dbm?: any, opt?: CommonDaoOptions): Promise<DBM>;
144
+ anyToDBMs(rows: DBM[], opt?: CommonDaoOptions): Promise<DBM[]>;
137
145
  /**
138
146
  * Returns *converted value* (NOT the same reference).
139
147
  * Does NOT mutate the object.
@@ -3,10 +3,11 @@ import { _uniqBy } from '@naturalcycles/js-lib/array/array.util.js';
3
3
  import { localTime } from '@naturalcycles/js-lib/datetime/localTime.js';
4
4
  import { _assert, ErrorMode } from '@naturalcycles/js-lib/error';
5
5
  import { _deepJsonEquals } from '@naturalcycles/js-lib/object/deepEquals.js';
6
- import { _filterUndefinedValues, _objectAssignExact, } from '@naturalcycles/js-lib/object/object.util.js';
6
+ import { _filterUndefinedValues, _objectAssignExact, _omitWithUndefined, _pick, } from '@naturalcycles/js-lib/object/object.util.js';
7
7
  import { pMap } from '@naturalcycles/js-lib/promise/pMap.js';
8
- import { _passthroughPredicate, _stringMapEntries, _stringMapValues, _typeCast, } from '@naturalcycles/js-lib/types';
8
+ import { _objectKeys, _passthroughPredicate, _stringMapEntries, _stringMapValues, _typeCast, } from '@naturalcycles/js-lib/types';
9
9
  import { stringId } from '@naturalcycles/nodejs-lib';
10
+ import { decompressZstdOrInflateToString, zstdCompress } from '@naturalcycles/nodejs-lib/zip';
10
11
  import { DBLibError } from '../cnst.js';
11
12
  import { RunnableDBQuery } from '../query/dbQuery.js';
12
13
  import { CommonDaoTransaction } from './commonDaoTransaction.js';
@@ -76,7 +77,7 @@ export class CommonDao {
76
77
  if (!id)
77
78
  return null;
78
79
  const [row] = await this.loadByIds([id], opt);
79
- return this.anyToDBM(row, opt) || null;
80
+ return await (this.anyToDBM(row, opt) || null);
80
81
  }
81
82
  async getByIds(ids, opt = {}) {
82
83
  const dbms = await this.loadByIds(ids, opt);
@@ -84,7 +85,7 @@ export class CommonDao {
84
85
  }
85
86
  async getByIdsAsDBM(ids, opt = {}) {
86
87
  const rows = await this.loadByIds(ids, opt);
87
- return this.anyToDBMs(rows);
88
+ return await this.anyToDBMs(rows);
88
89
  }
89
90
  // DRY private method
90
91
  async loadByIds(ids, opt = {}) {
@@ -149,7 +150,7 @@ export class CommonDao {
149
150
  q.table = opt.table || q.table;
150
151
  const { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
151
152
  const isPartialQuery = !!q._selectedFieldNames;
152
- const dbms = isPartialQuery ? rows : this.anyToDBMs(rows, opt);
153
+ const dbms = isPartialQuery ? rows : await this.anyToDBMs(rows, opt);
153
154
  return { rows: dbms, ...queryResult };
154
155
  }
155
156
  async runQueryCount(q, opt = {}) {
@@ -164,7 +165,7 @@ export class CommonDao {
164
165
  return pipeline;
165
166
  opt.skipValidation ??= true;
166
167
  opt.errorMode ||= ErrorMode.SUPPRESS;
167
- return pipeline.mapSync(dbm => this.anyToDBM(dbm, opt), { errorMode: opt.errorMode });
168
+ return pipeline.map(async (dbm) => await this.anyToDBM(dbm, opt), { errorMode: opt.errorMode });
168
169
  }
169
170
  streamQuery(q, opt = {}) {
170
171
  const pipeline = this.streamQueryRaw(q, opt);
@@ -339,7 +340,7 @@ export class CommonDao {
339
340
  async saveAsDBM(dbm, opt = {}) {
340
341
  this.requireWriteAccess();
341
342
  this.assignIdCreatedUpdated(dbm, opt); // mutates
342
- const row = this.anyToDBM(dbm, opt);
343
+ const row = await this.anyToDBM(dbm, opt);
343
344
  this.cfg.hooks.beforeSave?.(row);
344
345
  const table = opt.table || this.cfg.table;
345
346
  const saveOptions = this.prepareSaveOptions(opt);
@@ -371,7 +372,7 @@ export class CommonDao {
371
372
  return [];
372
373
  this.requireWriteAccess();
373
374
  dbms.forEach(dbm => this.assignIdCreatedUpdated(dbm, opt));
374
- const rows = this.anyToDBMs(dbms, opt);
375
+ const rows = await this.anyToDBMs(dbms, opt);
375
376
  if (this.cfg.hooks.beforeSave) {
376
377
  rows.forEach(row => this.cfg.hooks.beforeSave(row));
377
378
  }
@@ -538,6 +539,8 @@ export class CommonDao {
538
539
  // optimization: no need to run full joi DBM validation, cause BM validation will be run
539
540
  // const dbm = this.anyToDBM(_dbm, opt)
540
541
  const dbm = { ..._dbm, ...this.cfg.hooks.parseNaturalId(_dbm.id) };
542
+ // Decompress
543
+ await this.decompress(dbm);
541
544
  // DBM > BM
542
545
  const bm = ((await this.cfg.hooks.beforeDBMToBM?.(dbm)) || dbm);
543
546
  // Validate/convert BM
@@ -552,24 +555,62 @@ export class CommonDao {
552
555
  // bm gets assigned to the new reference
553
556
  bm = this.validateAndConvert(bm, 'save', opt);
554
557
  // BM > DBM
555
- return ((await this.cfg.hooks.beforeBMToDBM?.(bm)) || bm);
558
+ const dbm = ((await this.cfg.hooks.beforeBMToDBM?.(bm)) || bm);
559
+ // Compress
560
+ if (this.cfg.compress)
561
+ await this.compress(dbm);
562
+ return dbm;
556
563
  }
557
564
  async bmsToDBM(bms, opt = {}) {
558
565
  // try/catch?
559
566
  return await pMap(bms, async (bm) => await this.bmToDBM(bm, opt));
560
567
  }
561
- anyToDBM(dbm, _opt = {}) {
568
+ /**
569
+ * Mutates `dbm`.
570
+ */
571
+ async compress(dbm) {
572
+ if (!this.cfg.compress?.keys.length)
573
+ return; // No compression requested
574
+ const { keys } = this.cfg.compress;
575
+ const properties = _pick(dbm, keys);
576
+ _assert(!('data' in dbm) || 'data' in properties, `Data (${dbm.id}) already has a "data" property. When using compression, this property must be included in the compression keys list.`);
577
+ const bufferString = JSON.stringify(properties);
578
+ const data = await zstdCompress(bufferString);
579
+ _omitWithUndefined(dbm, _objectKeys(properties), { mutate: true });
580
+ Object.assign(dbm, { data });
581
+ }
582
+ /**
583
+ * Mutates `dbm`.
584
+ */
585
+ async decompress(dbm) {
586
+ _typeCast(dbm);
587
+ if (!this.cfg.compress)
588
+ return; // Auto-compression not turned on
589
+ if (!Buffer.isBuffer(dbm.data))
590
+ return; // No compressed data
591
+ // try-catch to avoid a `data` with Buffer which is not compressed, but legit data
592
+ try {
593
+ const bufferString = await decompressZstdOrInflateToString(dbm.data);
594
+ const properties = JSON.parse(bufferString);
595
+ dbm.data = undefined;
596
+ Object.assign(dbm, properties);
597
+ }
598
+ catch { }
599
+ }
600
+ async anyToDBM(dbm, _opt = {}) {
562
601
  if (!dbm)
563
602
  return null;
564
603
  // this shouldn't be happening on load! but should on save!
565
604
  // this.assignIdCreatedUpdated(dbm, opt)
566
605
  dbm = { ...dbm, ...this.cfg.hooks.parseNaturalId(dbm.id) };
606
+ // Decompress
607
+ await this.decompress(dbm);
567
608
  // Validate/convert DBM
568
609
  // return this.validateAndConvert(dbm, this.cfg.dbmSchema, DBModelType.DBM, opt)
569
610
  return dbm;
570
611
  }
571
- anyToDBMs(rows, opt = {}) {
572
- return rows.map(entity => this.anyToDBM(entity, opt));
612
+ async anyToDBMs(rows, opt = {}) {
613
+ return await pMap(rows, async (entity) => await this.anyToDBM(entity, opt));
573
614
  }
574
615
  /**
575
616
  * Returns *converted value* (NOT the same reference).
@@ -167,6 +167,19 @@ export interface CommonDaoCfg<BM extends BaseDBEntity, DBM extends BaseDBEntity
167
167
  * @experimental
168
168
  */
169
169
  patchInTransaction?: boolean;
170
+ /**
171
+ * When specified, the listed properties will be compressed under a `data` property in the DBM.
172
+ * If DBM already has a `data` property and you don't add it to the list, an error will be thrown.
173
+ *
174
+ * When specified with an empty `keys` list, then compression will be skipped, but all previously compressed data
175
+ * will be decompressed, so the Dao can still work.
176
+ *
177
+ * Compression happens after the `beforeBMToDBM` hook and before the DBM is saved to the database.
178
+ * Decompression happens after the DBM is loaded from the database and before the `beforeDBMToBM` hook.
179
+ */
180
+ compress?: {
181
+ keys: (keyof DBM)[];
182
+ };
170
183
  }
171
184
  /**
172
185
  * Index can be defined in simple form, just as a property name: `abc`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/db-lib",
3
3
  "type": "module",
4
- "version": "10.40.1",
4
+ "version": "10.41.1",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@naturalcycles/nodejs-lib": "^15"
@@ -205,6 +205,20 @@ export interface CommonDaoCfg<
205
205
  * @experimental
206
206
  */
207
207
  patchInTransaction?: boolean
208
+
209
+ /**
210
+ * When specified, the listed properties will be compressed under a `data` property in the DBM.
211
+ * If DBM already has a `data` property and you don't add it to the list, an error will be thrown.
212
+ *
213
+ * When specified with an empty `keys` list, then compression will be skipped, but all previously compressed data
214
+ * will be decompressed, so the Dao can still work.
215
+ *
216
+ * Compression happens after the `beforeBMToDBM` hook and before the DBM is saved to the database.
217
+ * Decompression happens after the DBM is loaded from the database and before the `beforeDBMToBM` hook.
218
+ */
219
+ compress?: {
220
+ keys: (keyof DBM)[]
221
+ }
208
222
  }
209
223
 
210
224
  /**
@@ -6,9 +6,12 @@ import { _deepJsonEquals } from '@naturalcycles/js-lib/object/deepEquals.js'
6
6
  import {
7
7
  _filterUndefinedValues,
8
8
  _objectAssignExact,
9
+ _omitWithUndefined,
10
+ _pick,
9
11
  } from '@naturalcycles/js-lib/object/object.util.js'
10
12
  import { pMap } from '@naturalcycles/js-lib/promise/pMap.js'
11
13
  import {
14
+ _objectKeys,
12
15
  _passthroughPredicate,
13
16
  _stringMapEntries,
14
17
  _stringMapValues,
@@ -22,6 +25,7 @@ import {
22
25
  import { stringId } from '@naturalcycles/nodejs-lib'
23
26
  import type { JsonSchema } from '@naturalcycles/nodejs-lib/ajv'
24
27
  import type { Pipeline } from '@naturalcycles/nodejs-lib/stream'
28
+ import { decompressZstdOrInflateToString, zstdCompress } from '@naturalcycles/nodejs-lib/zip'
25
29
  import { DBLibError } from '../cnst.js'
26
30
  import type {
27
31
  CommonDBSaveOptions,
@@ -118,7 +122,7 @@ export class CommonDao<
118
122
  async getByIdAsDBM(id?: ID | null, opt: CommonDaoReadOptions = {}): Promise<DBM | null> {
119
123
  if (!id) return null
120
124
  const [row] = await this.loadByIds([id], opt)
121
- return this.anyToDBM(row, opt) || null
125
+ return await (this.anyToDBM(row, opt) || null)
122
126
  }
123
127
 
124
128
  async getByIds(ids: ID[], opt: CommonDaoReadOptions = {}): Promise<BM[]> {
@@ -128,7 +132,7 @@ export class CommonDao<
128
132
 
129
133
  async getByIdsAsDBM(ids: ID[], opt: CommonDaoReadOptions = {}): Promise<DBM[]> {
130
134
  const rows = await this.loadByIds(ids, opt)
131
- return this.anyToDBMs(rows)
135
+ return await this.anyToDBMs(rows)
132
136
  }
133
137
 
134
138
  // DRY private method
@@ -216,7 +220,7 @@ export class CommonDao<
216
220
  q.table = opt.table || q.table
217
221
  const { rows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
218
222
  const isPartialQuery = !!q._selectedFieldNames
219
- const dbms = isPartialQuery ? rows : this.anyToDBMs(rows, opt)
223
+ const dbms = isPartialQuery ? rows : await this.anyToDBMs(rows, opt)
220
224
  return { rows: dbms, ...queryResult }
221
225
  }
222
226
 
@@ -235,7 +239,7 @@ export class CommonDao<
235
239
  opt.skipValidation ??= true
236
240
  opt.errorMode ||= ErrorMode.SUPPRESS
237
241
 
238
- return pipeline.mapSync(dbm => this.anyToDBM(dbm, opt), { errorMode: opt.errorMode })
242
+ return pipeline.map(async dbm => await this.anyToDBM(dbm, opt), { errorMode: opt.errorMode })
239
243
  }
240
244
 
241
245
  streamQuery(q: DBQuery<DBM>, opt: CommonDaoStreamOptions<BM> = {}): Pipeline<BM> {
@@ -455,7 +459,7 @@ export class CommonDao<
455
459
  async saveAsDBM(dbm: Unsaved<DBM>, opt: CommonDaoSaveOptions<BM, DBM> = {}): Promise<DBM> {
456
460
  this.requireWriteAccess()
457
461
  this.assignIdCreatedUpdated(dbm, opt) // mutates
458
- const row = this.anyToDBM(dbm, opt)
462
+ const row = await this.anyToDBM(dbm, opt)
459
463
  this.cfg.hooks!.beforeSave?.(row)
460
464
  const table = opt.table || this.cfg.table
461
465
  const saveOptions = this.prepareSaveOptions(opt)
@@ -496,7 +500,7 @@ export class CommonDao<
496
500
  if (!dbms.length) return []
497
501
  this.requireWriteAccess()
498
502
  dbms.forEach(dbm => this.assignIdCreatedUpdated(dbm, opt))
499
- const rows = this.anyToDBMs(dbms as DBM[], opt)
503
+ const rows = await this.anyToDBMs(dbms as DBM[], opt)
500
504
  if (this.cfg.hooks!.beforeSave) {
501
505
  rows.forEach(row => this.cfg.hooks!.beforeSave!(row))
502
506
  }
@@ -710,6 +714,9 @@ export class CommonDao<
710
714
  // const dbm = this.anyToDBM(_dbm, opt)
711
715
  const dbm: DBM = { ..._dbm, ...this.cfg.hooks!.parseNaturalId!(_dbm.id as ID) }
712
716
 
717
+ // Decompress
718
+ await this.decompress(dbm)
719
+
713
720
  // DBM > BM
714
721
  const bm = ((await this.cfg.hooks!.beforeDBMToBM?.(dbm)) || dbm) as Partial<BM>
715
722
 
@@ -734,7 +741,12 @@ export class CommonDao<
734
741
  bm = this.validateAndConvert(bm, 'save', opt)
735
742
 
736
743
  // BM > DBM
737
- return ((await this.cfg.hooks!.beforeBMToDBM?.(bm)) || bm) as DBM
744
+ const dbm = ((await this.cfg.hooks!.beforeBMToDBM?.(bm)) || bm) as DBM
745
+
746
+ // Compress
747
+ if (this.cfg.compress) await this.compress(dbm)
748
+
749
+ return dbm
738
750
  }
739
751
 
740
752
  async bmsToDBM(bms: BM[], opt: CommonDaoOptions = {}): Promise<DBM[]> {
@@ -742,9 +754,44 @@ export class CommonDao<
742
754
  return await pMap(bms, async bm => await this.bmToDBM(bm, opt))
743
755
  }
744
756
 
745
- anyToDBM(dbm: undefined, opt?: CommonDaoOptions): null
746
- anyToDBM(dbm?: any, opt?: CommonDaoOptions): DBM
747
- anyToDBM(dbm?: DBM, _opt: CommonDaoOptions = {}): DBM | null {
757
+ /**
758
+ * Mutates `dbm`.
759
+ */
760
+ async compress(dbm: DBM): Promise<void> {
761
+ if (!this.cfg.compress?.keys.length) return // No compression requested
762
+
763
+ const { keys } = this.cfg.compress
764
+ const properties = _pick(dbm, keys)
765
+ _assert(
766
+ !('data' in dbm) || 'data' in properties,
767
+ `Data (${dbm.id}) already has a "data" property. When using compression, this property must be included in the compression keys list.`,
768
+ )
769
+ const bufferString = JSON.stringify(properties)
770
+ const data = await zstdCompress(bufferString)
771
+ _omitWithUndefined(dbm as any, _objectKeys(properties), { mutate: true })
772
+ Object.assign(dbm, { data })
773
+ }
774
+
775
+ /**
776
+ * Mutates `dbm`.
777
+ */
778
+ async decompress(dbm: DBM): Promise<void> {
779
+ _typeCast<Compressed<DBM>>(dbm)
780
+ if (!this.cfg.compress) return // Auto-compression not turned on
781
+ if (!Buffer.isBuffer(dbm.data)) return // No compressed data
782
+
783
+ // try-catch to avoid a `data` with Buffer which is not compressed, but legit data
784
+ try {
785
+ const bufferString = await decompressZstdOrInflateToString(dbm.data)
786
+ const properties = JSON.parse(bufferString)
787
+ dbm.data = undefined
788
+ Object.assign(dbm, properties)
789
+ } catch {}
790
+ }
791
+
792
+ async anyToDBM(dbm: undefined, opt?: CommonDaoOptions): Promise<null>
793
+ async anyToDBM(dbm?: any, opt?: CommonDaoOptions): Promise<DBM>
794
+ async anyToDBM(dbm?: DBM, _opt: CommonDaoOptions = {}): Promise<DBM | null> {
748
795
  if (!dbm) return null
749
796
 
750
797
  // this shouldn't be happening on load! but should on save!
@@ -752,13 +799,16 @@ export class CommonDao<
752
799
 
753
800
  dbm = { ...dbm, ...this.cfg.hooks!.parseNaturalId!(dbm.id as ID) }
754
801
 
802
+ // Decompress
803
+ await this.decompress(dbm)
804
+
755
805
  // Validate/convert DBM
756
806
  // return this.validateAndConvert(dbm, this.cfg.dbmSchema, DBModelType.DBM, opt)
757
807
  return dbm
758
808
  }
759
809
 
760
- anyToDBMs(rows: DBM[], opt: CommonDaoOptions = {}): DBM[] {
761
- return rows.map(entity => this.anyToDBM(entity, opt))
810
+ async anyToDBMs(rows: DBM[], opt: CommonDaoOptions = {}): Promise<DBM[]> {
811
+ return await pMap(rows, async entity => await this.anyToDBM(entity, opt))
762
812
  }
763
813
 
764
814
  /**
@@ -1140,3 +1190,11 @@ export type InferDBM<DAO> = DAO extends CommonDao<any, infer DBM> ? DBM : never
1140
1190
  export type InferID<DAO> = DAO extends CommonDao<any, any, infer ID> ? ID : never
1141
1191
 
1142
1192
  export type AnyDao = CommonDao<any>
1193
+
1194
+ /**
1195
+ * Represents a DBM whose properties have been compressed into a `data` Buffer.
1196
+ *
1197
+ * Used internally during compression/decompression so that DBM instances can
1198
+ * carry their compressed payload alongside the original type shape.
1199
+ */
1200
+ type Compressed<DBM> = DBM & { data?: Buffer }