@naturalcycles/db-lib 8.53.0 → 8.54.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.
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LocalFilePersistencePlugin = void 0;
4
+ const fs = require("node:fs");
4
5
  const node_stream_1 = require("node:stream");
6
+ const fsp = require("node:fs/promises");
5
7
  const node_zlib_1 = require("node:zlib");
6
8
  const js_lib_1 = require("@naturalcycles/js-lib");
7
9
  const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
8
- const fs = require("fs-extra");
9
10
  /**
10
11
  * Persists in local filesystem as ndjson.
11
12
  */
@@ -19,15 +20,15 @@ class LocalFilePersistencePlugin {
19
20
  }
20
21
  async ping() { }
21
22
  async getTables() {
22
- return (await fs.readdir(this.cfg.storagePath))
23
+ return (await fsp.readdir(this.cfg.storagePath))
23
24
  .filter(f => f.includes('.ndjson'))
24
25
  .map(f => f.split('.ndjson')[0]);
25
26
  }
26
27
  async loadFile(table) {
27
- await fs.ensureDir(this.cfg.storagePath);
28
+ await (0, nodejs_lib_1._ensureDir)(this.cfg.storagePath);
28
29
  const ext = `ndjson${this.cfg.gzip ? '.gz' : ''}`;
29
30
  const filePath = `${this.cfg.storagePath}/${table}.${ext}`;
30
- if (!(await fs.pathExists(filePath)))
31
+ if (!(await (0, nodejs_lib_1._pathExists)(filePath)))
31
32
  return [];
32
33
  const transformUnzip = this.cfg.gzip ? [(0, node_zlib_1.createUnzip)()] : [];
33
34
  const rows = [];
@@ -44,7 +45,7 @@ class LocalFilePersistencePlugin {
44
45
  await (0, js_lib_1.pMap)(ops, async (op) => await this.saveFile(op.table, op.rows), { concurrency: 16 });
45
46
  }
46
47
  async saveFile(table, rows) {
47
- await fs.ensureDir(this.cfg.storagePath);
48
+ await (0, nodejs_lib_1._ensureDir)(this.cfg.storagePath);
48
49
  const ext = `ndjson${this.cfg.gzip ? '.gz' : ''}`;
49
50
  const filePath = `${this.cfg.storagePath}/${table}.${ext}`;
50
51
  const transformZip = this.cfg.gzip ? [(0, node_zlib_1.createGzip)()] : [];
@@ -1,12 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.InMemoryDB = void 0;
4
+ const fs = require("node:fs");
5
+ const fsp = require("node:fs/promises");
4
6
  const node_stream_1 = require("node:stream");
5
7
  const node_zlib_1 = require("node:zlib");
6
8
  const js_lib_1 = require("@naturalcycles/js-lib");
7
9
  const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
8
10
  const colors_1 = require("@naturalcycles/nodejs-lib/dist/colors");
9
- const fs = require("fs-extra");
10
11
  const __1 = require("../..");
11
12
  const dbQuery_1 = require("../../query/dbQuery");
12
13
  class InMemoryDB {
@@ -166,7 +167,7 @@ class InMemoryDB {
166
167
  (0, js_lib_1._assert)(this.cfg.persistenceEnabled, 'flushToDisk() called but persistenceEnabled=false');
167
168
  const { persistentStoragePath, persistZip } = this.cfg;
168
169
  const started = Date.now();
169
- await fs.emptyDir(persistentStoragePath);
170
+ await (0, nodejs_lib_1._emptyDir)(persistentStoragePath);
170
171
  const transformZip = persistZip ? [(0, node_zlib_1.createGzip)()] : [];
171
172
  let tables = 0;
172
173
  // infinite concurrency for now
@@ -192,9 +193,9 @@ class InMemoryDB {
192
193
  (0, js_lib_1._assert)(this.cfg.persistenceEnabled, 'restoreFromDisk() called but persistenceEnabled=false');
193
194
  const { persistentStoragePath } = this.cfg;
194
195
  const started = Date.now();
195
- await fs.ensureDir(persistentStoragePath);
196
+ await (0, nodejs_lib_1._ensureDir)(persistentStoragePath);
196
197
  this.data = {}; // empty it in the beginning!
197
- const files = (await fs.readdir(persistentStoragePath)).filter(f => f.includes('.ndjson'));
198
+ const files = (await fsp.readdir(persistentStoragePath)).filter(f => f.includes('.ndjson'));
198
199
  // infinite concurrency for now
199
200
  await (0, js_lib_1.pMap)(files, async (file) => {
200
201
  const fname = `${persistentStoragePath}/${file}`;
@@ -1,4 +1,4 @@
1
- import { AnyObject, AsyncMapper, JsonSchemaObject, JsonSchemaRootObject, ObjectWithId, Promisable, Saved, Unsaved, ZodSchema } from '@naturalcycles/js-lib';
1
+ import { AnyObject, AsyncMapper, JsonSchemaObject, JsonSchemaRootObject, ObjectWithId, Promisable, Saved, UnixTimestampMillisNumber, Unsaved, ZodSchema } from '@naturalcycles/js-lib';
2
2
  import { AjvSchema, ObjectSchemaTyped, ReadableTyped } from '@naturalcycles/nodejs-lib';
3
3
  import { DBDeleteByIdsOperation, DBModelType, DBOperation, DBPatch, DBSaveBatchOperation, RunQueryResult } from '../db.model';
4
4
  import { DBQuery, RunnableDBQuery } from '../query/dbQuery';
@@ -84,7 +84,7 @@ export declare class CommonDao<BM extends Partial<ObjectWithId<ID>>, DBM extends
84
84
  assignIdCreatedUpdated(obj: BM, opt?: CommonDaoOptions): Saved<BM>;
85
85
  assignIdCreatedUpdated(obj: Unsaved<BM>, opt?: CommonDaoOptions): Saved<BM>;
86
86
  tx: {
87
- save: (bm: Unsaved<BM>, opt?: CommonDaoSaveOptions<DBM>) => Promise<DBSaveBatchOperation>;
87
+ save: (bm: Unsaved<BM>, opt?: CommonDaoSaveOptions<DBM>) => Promise<DBSaveBatchOperation | undefined>;
88
88
  saveBatch: (bms: Unsaved<BM>[], opt?: CommonDaoSaveOptions<DBM>) => Promise<DBSaveBatchOperation | undefined>;
89
89
  deleteByIds: (ids: ID[], opt?: CommonDaoOptions) => Promise<DBDeleteByIdsOperation | undefined>;
90
90
  deleteById: (id: ID | null | undefined, opt?: CommonDaoOptions) => Promise<DBDeleteByIdsOperation | undefined>;
@@ -155,6 +155,6 @@ export declare class CommonDao<BM extends Partial<ObjectWithId<ID>>, DBM extends
155
155
  runInTransaction(ops: Promisable<DBOperation | undefined>[]): Promise<void>;
156
156
  protected logResult(started: number, op: string, res: any, table: string): void;
157
157
  protected logSaveResult(started: number, op: string, table: string): void;
158
- protected logStarted(op: string, table: string, force?: boolean): number;
159
- protected logSaveStarted(op: string, items: any, table: string): number;
158
+ protected logStarted(op: string, table: string, force?: boolean): UnixTimestampMillisNumber;
159
+ protected logSaveStarted(op: string, items: any, table: string): UnixTimestampMillisNumber;
160
160
  }
@@ -22,7 +22,10 @@ class CommonDao {
22
22
  this.cfg = cfg;
23
23
  this.tx = {
24
24
  save: async (bm, opt = {}) => {
25
+ // .save actually returns DBM (not BM) when it detects `opt.tx === true`
25
26
  const row = (await this.save(bm, { ...opt, tx: true }));
27
+ if (row === null)
28
+ return;
26
29
  return {
27
30
  type: 'saveBatch',
28
31
  table: this.cfg.table,
@@ -34,9 +37,9 @@ class CommonDao {
34
37
  };
35
38
  },
36
39
  saveBatch: async (bms, opt = {}) => {
37
- if (!bms.length)
38
- return;
39
40
  const rows = (await this.saveBatch(bms, { ...opt, tx: true }));
41
+ if (!rows.length)
42
+ return;
40
43
  return {
41
44
  type: 'saveBatch',
42
45
  table: this.cfg.table,
@@ -113,7 +116,10 @@ class CommonDao {
113
116
  const op = `getById(${id})`;
114
117
  const table = opt.table || this.cfg.table;
115
118
  const started = this.logStarted(op, table);
116
- const dbm = (await this.cfg.db.getByIds(table, [id]))[0];
119
+ let dbm = (await this.cfg.db.getByIds(table, [id]))[0];
120
+ if (dbm && !opt.raw && this.cfg.hooks.afterLoad) {
121
+ dbm = (await this.cfg.hooks.afterLoad(dbm)) || undefined;
122
+ }
117
123
  const bm = opt.raw ? dbm : await this.dbmToBM(dbm, opt);
118
124
  this.logResult(started, op, bm, table);
119
125
  return bm || null;
@@ -138,6 +144,9 @@ class CommonDao {
138
144
  const table = opt.table || this.cfg.table;
139
145
  const started = this.logStarted(op, table);
140
146
  let [dbm] = await this.cfg.db.getByIds(table, [id]);
147
+ if (dbm && !opt.raw && this.cfg.hooks.afterLoad) {
148
+ dbm = (await this.cfg.hooks.afterLoad(dbm)) || undefined;
149
+ }
141
150
  if (!opt.raw) {
142
151
  dbm = this.anyToDBM(dbm, opt);
143
152
  }
@@ -150,7 +159,10 @@ class CommonDao {
150
159
  const op = `getByIdAsTM(${id})`;
151
160
  const table = opt.table || this.cfg.table;
152
161
  const started = this.logStarted(op, table);
153
- const [dbm] = await this.cfg.db.getByIds(table, [id]);
162
+ let [dbm] = await this.cfg.db.getByIds(table, [id]);
163
+ if (dbm && !opt.raw && this.cfg.hooks.afterLoad) {
164
+ dbm = (await this.cfg.hooks.afterLoad(dbm)) || undefined;
165
+ }
154
166
  if (opt.raw) {
155
167
  this.logResult(started, op, dbm, table);
156
168
  return dbm || null;
@@ -161,19 +173,29 @@ class CommonDao {
161
173
  return tm || null;
162
174
  }
163
175
  async getByIds(ids, opt = {}) {
176
+ if (!ids.length)
177
+ return [];
164
178
  const op = `getByIds ${ids.length} id(s) (${(0, js_lib_1._truncate)(ids.slice(0, 10).join(', '), 50)})`;
165
179
  const table = opt.table || this.cfg.table;
166
180
  const started = this.logStarted(op, table);
167
- const dbms = await this.cfg.db.getByIds(table, ids);
181
+ let dbms = await this.cfg.db.getByIds(table, ids);
182
+ if (!opt.raw && this.cfg.hooks.afterLoad && dbms.length) {
183
+ dbms = (await (0, js_lib_1.pMap)(dbms, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(js_lib_1._isTruthy);
184
+ }
168
185
  const bms = opt.raw ? dbms : await this.dbmsToBM(dbms, opt);
169
186
  this.logResult(started, op, bms, table);
170
187
  return bms;
171
188
  }
172
189
  async getByIdsAsDBM(ids, opt = {}) {
190
+ if (!ids.length)
191
+ return [];
173
192
  const op = `getByIdsAsDBM ${ids.length} id(s) (${(0, js_lib_1._truncate)(ids.slice(0, 10).join(', '), 50)})`;
174
193
  const table = opt.table || this.cfg.table;
175
194
  const started = this.logStarted(op, table);
176
- const dbms = await this.cfg.db.getByIds(table, ids);
195
+ let dbms = await this.cfg.db.getByIds(table, ids);
196
+ if (!opt.raw && this.cfg.hooks.afterLoad && dbms.length) {
197
+ dbms = (await (0, js_lib_1.pMap)(dbms, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(js_lib_1._isTruthy);
198
+ }
177
199
  this.logResult(started, op, dbms, table);
178
200
  return dbms;
179
201
  }
@@ -272,8 +294,11 @@ class CommonDao {
272
294
  q.table = opt.table || q.table;
273
295
  const op = `runQuery(${q.pretty()})`;
274
296
  const started = this.logStarted(op, q.table);
275
- const { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
297
+ let { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
276
298
  const partialQuery = !!q._selectedFieldNames;
299
+ if (!opt.raw && this.cfg.hooks.afterLoad && rows.length) {
300
+ rows = (await (0, js_lib_1.pMap)(rows, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(js_lib_1._isTruthy);
301
+ }
277
302
  const bms = partialQuery || opt.raw ? rows : await this.dbmsToBM(rows, opt);
278
303
  this.logResult(started, op, bms, q.table);
279
304
  return {
@@ -289,7 +314,10 @@ class CommonDao {
289
314
  q.table = opt.table || q.table;
290
315
  const op = `runQueryAsDBM(${q.pretty()})`;
291
316
  const started = this.logStarted(op, q.table);
292
- const { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
317
+ let { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
318
+ if (!opt.raw && this.cfg.hooks.afterLoad && rows.length) {
319
+ rows = (await (0, js_lib_1.pMap)(rows, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(js_lib_1._isTruthy);
320
+ }
293
321
  const partialQuery = !!q._selectedFieldNames;
294
322
  const dbms = partialQuery || opt.raw ? rows : this.anyToDBMs(rows, opt);
295
323
  this.logResult(started, op, dbms, q.table);
@@ -303,7 +331,10 @@ class CommonDao {
303
331
  q.table = opt.table || q.table;
304
332
  const op = `runQueryAsTM(${q.pretty()})`;
305
333
  const started = this.logStarted(op, q.table);
306
- const { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
334
+ let { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
335
+ if (!opt.raw && this.cfg.hooks.afterLoad && rows.length) {
336
+ rows = (await (0, js_lib_1.pMap)(rows, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(js_lib_1._isTruthy);
337
+ }
307
338
  const partialQuery = !!q._selectedFieldNames;
308
339
  const tms = partialQuery || opt.raw ? rows : this.bmsToTM(await this.dbmsToBM(rows, opt), opt);
309
340
  this.logResult(started, op, tms, q.table);
@@ -333,11 +364,16 @@ class CommonDao {
333
364
  let count = 0;
334
365
  await (0, nodejs_lib_1._pipeline)([
335
366
  this.cfg.db.streamQuery(q, opt),
336
- // optimization: 1 validation is enough
337
- // transformMap<any, DBM>(dbm => (partialQuery || opt.raw ? dbm : this.anyToDBM(dbm, opt)), opt),
338
367
  (0, nodejs_lib_1.transformMap)(async (dbm) => {
339
368
  count++;
340
- return partialQuery || opt.raw ? dbm : await this.dbmToBM(dbm, opt);
369
+ if (partialQuery || opt.raw)
370
+ return dbm;
371
+ if (this.cfg.hooks.afterLoad) {
372
+ dbm = (await this.cfg.hooks.afterLoad(dbm));
373
+ if (dbm === null)
374
+ return js_lib_1.SKIP;
375
+ }
376
+ return await this.dbmToBM(dbm, opt);
341
377
  }, {
342
378
  errorMode: opt.errorMode,
343
379
  }),
@@ -367,9 +403,16 @@ class CommonDao {
367
403
  let count = 0;
368
404
  await (0, nodejs_lib_1._pipeline)([
369
405
  this.cfg.db.streamQuery(q, opt),
370
- (0, nodejs_lib_1.transformMapSync)(dbm => {
406
+ (0, nodejs_lib_1.transformMap)(async (dbm) => {
371
407
  count++;
372
- return partialQuery || opt.raw ? dbm : this.anyToDBM(dbm, opt);
408
+ if (partialQuery || opt.raw)
409
+ return dbm;
410
+ if (this.cfg.hooks.afterLoad) {
411
+ dbm = (await this.cfg.hooks.afterLoad(dbm));
412
+ if (dbm === null)
413
+ return js_lib_1.SKIP;
414
+ }
415
+ return this.anyToDBM(dbm, opt);
373
416
  }, {
374
417
  errorMode: opt.errorMode,
375
418
  }),
@@ -402,8 +445,15 @@ class CommonDao {
402
445
  return stream;
403
446
  return stream
404
447
  .on('error', err => stream.emit('error', err))
405
- .pipe((0, nodejs_lib_1.transformMapSimple)(dbm => this.anyToDBM(dbm, opt), {
406
- errorMode: js_lib_1.ErrorMode.SUPPRESS, // cause .pipe() cannot propagate errors
448
+ .pipe((0, nodejs_lib_1.transformMap)(async (dbm) => {
449
+ if (this.cfg.hooks.afterLoad) {
450
+ dbm = (await this.cfg.hooks.afterLoad(dbm));
451
+ if (dbm === null)
452
+ return js_lib_1.SKIP;
453
+ }
454
+ return this.anyToDBM(dbm, opt);
455
+ }, {
456
+ errorMode: opt.errorMode,
407
457
  }));
408
458
  }
409
459
  /**
@@ -429,8 +479,15 @@ class CommonDao {
429
479
  // .pipe(transformMap<any, DBM>(dbm => this.anyToDBM(dbm, opt), safeOpt))
430
480
  // .pipe(transformMap<DBM, Saved<BM>>(dbm => this.dbmToBM(dbm, opt), safeOpt))
431
481
  .on('error', err => stream.emit('error', err))
432
- .pipe((0, nodejs_lib_1.transformMap)(async (dbm) => await this.dbmToBM(dbm, opt), {
433
- errorMode: js_lib_1.ErrorMode.SUPPRESS, // cause .pipe() cannot propagate errors
482
+ .pipe((0, nodejs_lib_1.transformMap)(async (dbm) => {
483
+ if (this.cfg.hooks.afterLoad) {
484
+ dbm = (await this.cfg.hooks.afterLoad(dbm));
485
+ if (dbm === null)
486
+ return js_lib_1.SKIP;
487
+ }
488
+ return await this.dbmToBM(dbm, opt);
489
+ }, {
490
+ errorMode: opt.errorMode,
434
491
  }))
435
492
  // this can make the stream async-iteration-friendly
436
493
  // but not applying it now for perf reasons
@@ -461,8 +518,10 @@ class CommonDao {
461
518
  let count = 0;
462
519
  await (0, nodejs_lib_1._pipeline)([
463
520
  this.cfg.db.streamQuery(q.select(['id']), opt),
464
- (0, nodejs_lib_1.transformMapSimple)(objectWithId => objectWithId.id),
465
- (0, nodejs_lib_1.transformTap)(() => count++),
521
+ (0, nodejs_lib_1.transformMapSimple)(objectWithId => {
522
+ count++;
523
+ return objectWithId.id;
524
+ }),
466
525
  (0, nodejs_lib_1.transformMap)(mapper, {
467
526
  ...opt,
468
527
  predicate: js_lib_1._passthroughPredicate,
@@ -502,8 +561,14 @@ class CommonDao {
502
561
  this.requireWriteAccess();
503
562
  const idWasGenerated = !bm.id && this.cfg.createId;
504
563
  this.assignIdCreatedUpdated(bm, opt); // mutates
505
- const dbm = await this.bmToDBM(bm, opt);
564
+ let dbm = await this.bmToDBM(bm, opt);
565
+ if (this.cfg.hooks.beforeSave) {
566
+ dbm = (await this.cfg.hooks.beforeSave(dbm));
567
+ if (dbm === null && !opt.tx)
568
+ return bm;
569
+ }
506
570
  if (opt.tx) {
571
+ // May return `null`, in which case it'll be skipped
507
572
  return dbm;
508
573
  }
509
574
  const table = opt.table || this.cfg.table;
@@ -569,6 +634,11 @@ class CommonDao {
569
634
  const started = this.logSaveStarted(op, row, table);
570
635
  const { excludeFromIndexes } = this.cfg;
571
636
  const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds;
637
+ if (this.cfg.hooks.beforeSave) {
638
+ row = (await this.cfg.hooks.beforeSave(row));
639
+ if (row === null)
640
+ return dbm;
641
+ }
572
642
  await this.cfg.db.saveBatch(table, [row], {
573
643
  excludeFromIndexes,
574
644
  assignGeneratedIds,
@@ -581,10 +651,15 @@ class CommonDao {
581
651
  return row;
582
652
  }
583
653
  async saveBatch(bms, opt = {}) {
654
+ if (!bms.length)
655
+ return [];
584
656
  this.requireWriteAccess();
585
657
  const table = opt.table || this.cfg.table;
586
658
  bms.forEach(bm => this.assignIdCreatedUpdated(bm, opt));
587
- const dbms = await this.bmsToDBM(bms, opt);
659
+ let dbms = await this.bmsToDBM(bms, opt);
660
+ if (this.cfg.hooks.beforeSave && dbms.length) {
661
+ dbms = (await (0, js_lib_1.pMap)(dbms, async (dbm) => await this.cfg.hooks.beforeSave(dbm))).filter(js_lib_1._isTruthy);
662
+ }
588
663
  if (opt.tx) {
589
664
  return dbms;
590
665
  }
@@ -612,6 +687,8 @@ class CommonDao {
612
687
  return bms;
613
688
  }
614
689
  async saveBatchAsDBM(dbms, opt = {}) {
690
+ if (!dbms.length)
691
+ return [];
615
692
  this.requireWriteAccess();
616
693
  const table = opt.table || this.cfg.table;
617
694
  let rows = dbms;
@@ -631,6 +708,9 @@ class CommonDao {
631
708
  const started = this.logSaveStarted(op, rows, table);
632
709
  const { excludeFromIndexes } = this.cfg;
633
710
  const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds;
711
+ if (this.cfg.hooks.beforeSave && rows.length) {
712
+ rows = (await (0, js_lib_1.pMap)(rows, async (row) => await this.cfg.hooks.beforeSave(row))).filter(js_lib_1._isTruthy);
713
+ }
634
714
  await this.cfg.db.saveBatch(table, rows, {
635
715
  excludeFromIndexes,
636
716
  assignGeneratedIds,
@@ -655,6 +735,8 @@ class CommonDao {
655
735
  return count;
656
736
  }
657
737
  async deleteByIds(ids, opt = {}) {
738
+ if (!ids.length)
739
+ return 0;
658
740
  this.requireWriteAccess();
659
741
  this.requireObjectMutability(opt);
660
742
  const op = `deleteByIds(${ids.join(', ')})`;
@@ -709,6 +791,8 @@ class CommonDao {
709
791
  return await this.updateByQuery(this.query().filterEq('id', id), patch, opt);
710
792
  }
711
793
  async updateByIds(ids, patch, opt = {}) {
794
+ if (!ids.length)
795
+ return 0;
712
796
  return await this.updateByQuery(this.query().filterIn('id', ids), patch, opt);
713
797
  }
714
798
  async updateByQuery(q, patch, opt = {}) {
@@ -1,4 +1,4 @@
1
- import { CommonLogger, ErrorMode, ObjectWithId, Saved, ZodError, ZodSchema } from '@naturalcycles/js-lib';
1
+ import { CommonLogger, ErrorMode, ObjectWithId, Promisable, Saved, ZodError, ZodSchema } from '@naturalcycles/js-lib';
2
2
  import { AjvSchema, AjvValidationError, JoiValidationError, ObjectSchemaTyped, TransformLogProgressOptions, TransformMapOptions } from '@naturalcycles/nodejs-lib';
3
3
  import { CommonDB } from '../common.db';
4
4
  import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions } from '../db.model';
@@ -30,7 +30,7 @@ export interface CommonDaoHooks<BM extends Partial<ObjectWithId<ID>>, DBM extend
30
30
  beforeCreate: (bm: Partial<BM>) => Partial<BM>;
31
31
  /**
32
32
  * Called when loading things "as DBM" and validation is not skipped.
33
- * When loading things like BM/TM - other hooks get involved instead:
33
+ * When loading things as BM/TM - other hooks get involved instead:
34
34
  * - beforeDBMToBM
35
35
  * - beforeBMToTM
36
36
  *
@@ -41,6 +41,33 @@ export interface CommonDaoHooks<BM extends Partial<ObjectWithId<ID>>, DBM extend
41
41
  beforeDBMToBM: (dbm: DBM) => Partial<BM> | Promise<Partial<BM>>;
42
42
  beforeBMToDBM: (bm: BM) => Partial<DBM> | Promise<Partial<DBM>>;
43
43
  beforeBMToTM: (bm: BM) => Partial<TM>;
44
+ /**
45
+ * Allows to access the DBM just after it has been loaded from the DB.
46
+ *
47
+ * Normally does nothing.
48
+ *
49
+ * You can change the DBM as you want here: ok to mutate or not, but you need to return the DBM
50
+ * to pass it further.
51
+ *
52
+ * You can return `null` to make it look "not found".
53
+ *
54
+ * You can do validations as needed here and throw errors, they will be propagated.
55
+ */
56
+ afterLoad?: (dbm: DBM) => Promisable<DBM | null>;
57
+ /**
58
+ * Allows to access the DBM just before it's supposed to be saved to the DB.
59
+ *
60
+ * Normally does nothing.
61
+ *
62
+ * You can change the DBM as you want here: ok to mutate or not, but you need to return the DBM
63
+ * to pass it further.
64
+ *
65
+ * You can return `null` to prevent it from being saved, without throwing an error.
66
+ * `.save` method will then return the BM/DBM as it has entered the method (it **won't** return the null value!).
67
+ *
68
+ * You can do validations as needed here and throw errors, they will be propagated.
69
+ */
70
+ beforeSave?: (dbm: DBM) => Promisable<DBM | null>;
44
71
  /**
45
72
  * Called in:
46
73
  * - dbmToBM (applied before DBM becomes BM)
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.dbPipelineBackup = void 0;
4
+ const fs = require("node:fs");
5
+ const fsp = require("node:fs/promises");
4
6
  const node_zlib_1 = require("node:zlib");
5
7
  const js_lib_1 = require("@naturalcycles/js-lib");
6
8
  const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
7
9
  const colors_1 = require("@naturalcycles/nodejs-lib/dist/colors");
8
- const fs = require("fs-extra");
9
10
  const index_1 = require("../index");
10
11
  /**
11
12
  * Pipeline from input stream(s) to a NDJSON file (optionally gzipped).
@@ -23,7 +24,7 @@ async function dbPipelineBackup(opt) {
23
24
  let { tables } = opt;
24
25
  const sinceUpdatedStr = sinceUpdated ? ' since ' + (0, colors_1.grey)((0, js_lib_1.localTime)(sinceUpdated).toPretty()) : '';
25
26
  console.log(`>> ${(0, colors_1.dimWhite)('dbPipelineBackup')} started in ${(0, colors_1.grey)(outputDirPath)}...${sinceUpdatedStr}`);
26
- fs.ensureDirSync(outputDirPath);
27
+ (0, nodejs_lib_1._ensureDirSync)(outputDirPath);
27
28
  tables ||= await db.getTables();
28
29
  console.log(`${(0, colors_1.yellow)(tables.length)} ${(0, colors_1.boldWhite)('table(s)')}:\n` + tables.join('\n'));
29
30
  const statsPerTable = {};
@@ -34,16 +35,16 @@ async function dbPipelineBackup(opt) {
34
35
  }
35
36
  const filePath = `${outputDirPath}/${table}.ndjson` + (gzip ? '.gz' : '');
36
37
  const schemaFilePath = `${outputDirPath}/${table}.schema.json`;
37
- if (protectFromOverwrite && (await fs.pathExists(filePath))) {
38
+ if (protectFromOverwrite && (0, nodejs_lib_1._pathExistsSync)(filePath)) {
38
39
  throw new js_lib_1.AppError(`dbPipelineBackup: output file exists: ${filePath}`);
39
40
  }
40
41
  const started = Date.now();
41
42
  let rows = 0;
42
- await fs.ensureFile(filePath);
43
+ (0, nodejs_lib_1._ensureFileSync)(filePath);
43
44
  console.log(`>> ${(0, colors_1.grey)(filePath)} started...`);
44
45
  if (emitSchemaFromDB) {
45
46
  const schema = await db.getTableSchema(table);
46
- await fs.writeJson(schemaFilePath, schema, { spaces: 2 });
47
+ await (0, nodejs_lib_1._writeJsonFile)(schemaFilePath, schema, { spaces: 2 });
47
48
  console.log(`>> ${(0, colors_1.grey)(schemaFilePath)} saved (generated from DB)`);
48
49
  }
49
50
  await (0, nodejs_lib_1._pipeline)([
@@ -66,7 +67,7 @@ async function dbPipelineBackup(opt) {
66
67
  ...(gzip ? [(0, node_zlib_1.createGzip)(zlibOptions)] : []),
67
68
  fs.createWriteStream(filePath),
68
69
  ]);
69
- const { size: sizeBytes } = await fs.stat(filePath);
70
+ const { size: sizeBytes } = await fsp.stat(filePath);
70
71
  const stats = nodejs_lib_1.NDJsonStats.create({
71
72
  tookMillis: Date.now() - started,
72
73
  rows,
@@ -1,11 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.dbPipelineRestore = void 0;
4
+ const fs = require("node:fs");
4
5
  const node_zlib_1 = require("node:zlib");
5
6
  const js_lib_1 = require("@naturalcycles/js-lib");
6
7
  const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
7
8
  const colors_1 = require("@naturalcycles/nodejs-lib/dist/colors");
8
- const fs = require("fs-extra");
9
9
  /**
10
10
  * Pipeline from NDJSON files in a folder (optionally gzipped) to CommonDB.
11
11
  * Allows to define a mapper and a predicate to map/filter objects between input and output.
@@ -19,7 +19,7 @@ async function dbPipelineRestore(opt) {
19
19
  const onlyTables = opt.tables && new Set(opt.tables);
20
20
  const sinceUpdatedStr = sinceUpdated ? ' since ' + (0, colors_1.grey)((0, js_lib_1.localTime)(sinceUpdated).toPretty()) : '';
21
21
  console.log(`>> ${(0, colors_1.dimWhite)('dbPipelineRestore')} started in ${(0, colors_1.grey)(inputDirPath)}...${sinceUpdatedStr}`);
22
- fs.ensureDirSync(inputDirPath);
22
+ (0, nodejs_lib_1._ensureDirSync)(inputDirPath);
23
23
  const tablesToGzip = new Set();
24
24
  const sizeByTable = {};
25
25
  const statsPerTable = {};
@@ -54,7 +54,7 @@ async function dbPipelineRestore(opt) {
54
54
  console.warn(`${schemaFilePath} does not exist!`);
55
55
  return;
56
56
  }
57
- const schema = await fs.readJson(schemaFilePath);
57
+ const schema = await (0, nodejs_lib_1._readJsonFile)(schemaFilePath);
58
58
  await db.createTable(table, schema, { dropIfExists: true });
59
59
  });
60
60
  }
package/package.json CHANGED
@@ -5,13 +5,12 @@
5
5
  },
6
6
  "dependencies": {
7
7
  "@naturalcycles/js-lib": "^14.116.0",
8
- "@naturalcycles/nodejs-lib": "^12.0.0",
9
- "fs-extra": "^11.1.0"
8
+ "@naturalcycles/nodejs-lib": "^12.0.0"
10
9
  },
11
10
  "devDependencies": {
12
11
  "@naturalcycles/bench-lib": "^1.0.0",
13
12
  "@naturalcycles/dev-lib": "^13.0.0",
14
- "@types/node": "^18.0.3",
13
+ "@types/node": "^20.2.1",
15
14
  "jest": "^29.0.0"
16
15
  },
17
16
  "files": [
@@ -41,7 +40,7 @@
41
40
  "engines": {
42
41
  "node": ">=18.12"
43
42
  },
44
- "version": "8.53.0",
43
+ "version": "8.54.1",
45
44
  "description": "Lowest Common Denominator API to supported Databases",
46
45
  "keywords": [
47
46
  "db",
@@ -1,4 +1,6 @@
1
+ import * as fs from 'node:fs'
1
2
  import { Readable } from 'node:stream'
3
+ import * as fsp from 'node:fs/promises'
2
4
  import { createGzip, createUnzip } from 'node:zlib'
3
5
  import { pMap, ObjectWithId } from '@naturalcycles/js-lib'
4
6
  import {
@@ -7,8 +9,9 @@ import {
7
9
  transformToNDJson,
8
10
  writablePushToArray,
9
11
  _pipeline,
12
+ _ensureDir,
13
+ _pathExists,
10
14
  } from '@naturalcycles/nodejs-lib'
11
- import * as fs from 'fs-extra'
12
15
  import { DBSaveBatchOperation } from '../../db.model'
13
16
  import { FileDBPersistencePlugin } from './file.db.model'
14
17
 
@@ -41,17 +44,17 @@ export class LocalFilePersistencePlugin implements FileDBPersistencePlugin {
41
44
  async ping(): Promise<void> {}
42
45
 
43
46
  async getTables(): Promise<string[]> {
44
- return (await fs.readdir(this.cfg.storagePath))
47
+ return (await fsp.readdir(this.cfg.storagePath))
45
48
  .filter(f => f.includes('.ndjson'))
46
49
  .map(f => f.split('.ndjson')[0]!)
47
50
  }
48
51
 
49
52
  async loadFile<ROW extends ObjectWithId>(table: string): Promise<ROW[]> {
50
- await fs.ensureDir(this.cfg.storagePath)
53
+ await _ensureDir(this.cfg.storagePath)
51
54
  const ext = `ndjson${this.cfg.gzip ? '.gz' : ''}`
52
55
  const filePath = `${this.cfg.storagePath}/${table}.${ext}`
53
56
 
54
- if (!(await fs.pathExists(filePath))) return []
57
+ if (!(await _pathExists(filePath))) return []
55
58
 
56
59
  const transformUnzip = this.cfg.gzip ? [createUnzip()] : []
57
60
 
@@ -73,7 +76,7 @@ export class LocalFilePersistencePlugin implements FileDBPersistencePlugin {
73
76
  }
74
77
 
75
78
  async saveFile<ROW extends ObjectWithId>(table: string, rows: ROW[]): Promise<void> {
76
- await fs.ensureDir(this.cfg.storagePath)
79
+ await _ensureDir(this.cfg.storagePath)
77
80
  const ext = `ndjson${this.cfg.gzip ? '.gz' : ''}`
78
81
  const filePath = `${this.cfg.storagePath}/${table}.${ext}`
79
82
  const transformZip = this.cfg.gzip ? [createGzip()] : []
@@ -1,3 +1,5 @@
1
+ import * as fs from 'node:fs'
2
+ import * as fsp from 'node:fs/promises'
1
3
  import { Readable } from 'node:stream'
2
4
  import { createGzip, createUnzip } from 'node:zlib'
3
5
  import {
@@ -23,9 +25,10 @@ import {
23
25
  transformToNDJson,
24
26
  writablePushToArray,
25
27
  _pipeline,
28
+ _emptyDir,
29
+ _ensureDir,
26
30
  } from '@naturalcycles/nodejs-lib'
27
31
  import { dimGrey, yellow } from '@naturalcycles/nodejs-lib/dist/colors'
28
- import * as fs from 'fs-extra'
29
32
  import { CommonDB, DBIncrement, DBPatch, DBTransaction, queryInMemory } from '../..'
30
33
  import {
31
34
  CommonDBCreateOptions,
@@ -279,7 +282,7 @@ export class InMemoryDB implements CommonDB {
279
282
 
280
283
  const started = Date.now()
281
284
 
282
- await fs.emptyDir(persistentStoragePath)
285
+ await _emptyDir(persistentStoragePath)
283
286
 
284
287
  const transformZip = persistZip ? [createGzip()] : []
285
288
  let tables = 0
@@ -314,11 +317,11 @@ export class InMemoryDB implements CommonDB {
314
317
 
315
318
  const started = Date.now()
316
319
 
317
- await fs.ensureDir(persistentStoragePath)
320
+ await _ensureDir(persistentStoragePath)
318
321
 
319
322
  this.data = {} // empty it in the beginning!
320
323
 
321
- const files = (await fs.readdir(persistentStoragePath)).filter(f => f.includes('.ndjson'))
324
+ const files = (await fsp.readdir(persistentStoragePath)).filter(f => f.includes('.ndjson'))
322
325
 
323
326
  // infinite concurrency for now
324
327
  await pMap(files, async file => {
@@ -2,6 +2,7 @@ import {
2
2
  CommonLogger,
3
3
  ErrorMode,
4
4
  ObjectWithId,
5
+ Promisable,
5
6
  Saved,
6
7
  ZodError,
7
8
  ZodSchema,
@@ -54,7 +55,7 @@ export interface CommonDaoHooks<
54
55
 
55
56
  /**
56
57
  * Called when loading things "as DBM" and validation is not skipped.
57
- * When loading things like BM/TM - other hooks get involved instead:
58
+ * When loading things as BM/TM - other hooks get involved instead:
58
59
  * - beforeDBMToBM
59
60
  * - beforeBMToTM
60
61
  *
@@ -67,6 +68,35 @@ export interface CommonDaoHooks<
67
68
  beforeBMToDBM: (bm: BM) => Partial<DBM> | Promise<Partial<DBM>>
68
69
  beforeBMToTM: (bm: BM) => Partial<TM>
69
70
 
71
+ /**
72
+ * Allows to access the DBM just after it has been loaded from the DB.
73
+ *
74
+ * Normally does nothing.
75
+ *
76
+ * You can change the DBM as you want here: ok to mutate or not, but you need to return the DBM
77
+ * to pass it further.
78
+ *
79
+ * You can return `null` to make it look "not found".
80
+ *
81
+ * You can do validations as needed here and throw errors, they will be propagated.
82
+ */
83
+ afterLoad?: (dbm: DBM) => Promisable<DBM | null>
84
+
85
+ /**
86
+ * Allows to access the DBM just before it's supposed to be saved to the DB.
87
+ *
88
+ * Normally does nothing.
89
+ *
90
+ * You can change the DBM as you want here: ok to mutate or not, but you need to return the DBM
91
+ * to pass it further.
92
+ *
93
+ * You can return `null` to prevent it from being saved, without throwing an error.
94
+ * `.save` method will then return the BM/DBM as it has entered the method (it **won't** return the null value!).
95
+ *
96
+ * You can do validations as needed here and throw errors, they will be propagated.
97
+ */
98
+ beforeSave?: (dbm: DBM) => Promisable<DBM | null>
99
+
70
100
  /**
71
101
  * Called in:
72
102
  * - dbmToBM (applied before DBM becomes BM)
@@ -17,6 +17,8 @@ import {
17
17
  pMap,
18
18
  Promisable,
19
19
  Saved,
20
+ SKIP,
21
+ UnixTimestampMillisNumber,
20
22
  Unsaved,
21
23
  ZodSchema,
22
24
  ZodValidationError,
@@ -35,8 +37,6 @@ import {
35
37
  transformLogProgress,
36
38
  transformMap,
37
39
  transformMapSimple,
38
- transformMapSync,
39
- transformTap,
40
40
  writableVoid,
41
41
  } from '@naturalcycles/nodejs-lib'
42
42
  import { DBLibError } from '../cnst'
@@ -53,6 +53,7 @@ import { DBTransaction } from '../transaction/dbTransaction'
53
53
  import {
54
54
  CommonDaoCfg,
55
55
  CommonDaoCreateOptions,
56
+ CommonDaoHooks,
56
57
  CommonDaoLogLevel,
57
58
  CommonDaoOptions,
58
59
  CommonDaoSaveOptions,
@@ -99,7 +100,7 @@ export class CommonDao<
99
100
  anonymize: dbm => dbm,
100
101
  onValidationError: err => err,
101
102
  ...cfg.hooks,
102
- },
103
+ } satisfies Partial<CommonDaoHooks<BM, DBM, TM, ID>>,
103
104
  }
104
105
 
105
106
  if (this.cfg.createId) {
@@ -131,7 +132,10 @@ export class CommonDao<
131
132
  const table = opt.table || this.cfg.table
132
133
  const started = this.logStarted(op, table)
133
134
 
134
- const dbm = (await this.cfg.db.getByIds<DBM>(table, [id]))[0]
135
+ let dbm = (await this.cfg.db.getByIds<DBM>(table, [id]))[0]
136
+ if (dbm && !opt.raw && this.cfg.hooks!.afterLoad) {
137
+ dbm = (await this.cfg.hooks!.afterLoad(dbm)) || undefined
138
+ }
135
139
 
136
140
  const bm = opt.raw ? (dbm as any) : await this.dbmToBM(dbm, opt)
137
141
  this.logResult(started, op, bm, table)
@@ -161,6 +165,10 @@ export class CommonDao<
161
165
  const table = opt.table || this.cfg.table
162
166
  const started = this.logStarted(op, table)
163
167
  let [dbm] = await this.cfg.db.getByIds<DBM>(table, [id])
168
+ if (dbm && !opt.raw && this.cfg.hooks!.afterLoad) {
169
+ dbm = (await this.cfg.hooks!.afterLoad(dbm)) || undefined
170
+ }
171
+
164
172
  if (!opt.raw) {
165
173
  dbm = this.anyToDBM(dbm!, opt)
166
174
  }
@@ -175,7 +183,11 @@ export class CommonDao<
175
183
  const op = `getByIdAsTM(${id})`
176
184
  const table = opt.table || this.cfg.table
177
185
  const started = this.logStarted(op, table)
178
- const [dbm] = await this.cfg.db.getByIds<DBM>(table, [id])
186
+ let [dbm] = await this.cfg.db.getByIds<DBM>(table, [id])
187
+ if (dbm && !opt.raw && this.cfg.hooks!.afterLoad) {
188
+ dbm = (await this.cfg.hooks!.afterLoad(dbm)) || undefined
189
+ }
190
+
179
191
  if (opt.raw) {
180
192
  this.logResult(started, op, dbm, table)
181
193
  return (dbm as any) || null
@@ -187,20 +199,34 @@ export class CommonDao<
187
199
  }
188
200
 
189
201
  async getByIds(ids: ID[], opt: CommonDaoOptions = {}): Promise<Saved<BM>[]> {
202
+ if (!ids.length) return []
190
203
  const op = `getByIds ${ids.length} id(s) (${_truncate(ids.slice(0, 10).join(', '), 50)})`
191
204
  const table = opt.table || this.cfg.table
192
205
  const started = this.logStarted(op, table)
193
- const dbms = await this.cfg.db.getByIds<DBM>(table, ids)
206
+ let dbms = await this.cfg.db.getByIds<DBM>(table, ids)
207
+ if (!opt.raw && this.cfg.hooks!.afterLoad && dbms.length) {
208
+ dbms = (await pMap(dbms, async dbm => await this.cfg.hooks!.afterLoad!(dbm))).filter(
209
+ _isTruthy,
210
+ )
211
+ }
212
+
194
213
  const bms = opt.raw ? (dbms as any) : await this.dbmsToBM(dbms, opt)
195
214
  this.logResult(started, op, bms, table)
196
215
  return bms
197
216
  }
198
217
 
199
218
  async getByIdsAsDBM(ids: ID[], opt: CommonDaoOptions = {}): Promise<DBM[]> {
219
+ if (!ids.length) return []
200
220
  const op = `getByIdsAsDBM ${ids.length} id(s) (${_truncate(ids.slice(0, 10).join(', '), 50)})`
201
221
  const table = opt.table || this.cfg.table
202
222
  const started = this.logStarted(op, table)
203
- const dbms = await this.cfg.db.getByIds<DBM>(table, ids)
223
+ let dbms = await this.cfg.db.getByIds<DBM>(table, ids)
224
+ if (!opt.raw && this.cfg.hooks!.afterLoad && dbms.length) {
225
+ dbms = (await pMap(dbms, async dbm => await this.cfg.hooks!.afterLoad!(dbm))).filter(
226
+ _isTruthy,
227
+ )
228
+ }
229
+
204
230
  this.logResult(started, op, dbms, table)
205
231
  return dbms
206
232
  }
@@ -323,8 +349,14 @@ export class CommonDao<
323
349
  q.table = opt.table || q.table
324
350
  const op = `runQuery(${q.pretty()})`
325
351
  const started = this.logStarted(op, q.table)
326
- const { rows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
352
+ let { rows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
327
353
  const partialQuery = !!q._selectedFieldNames
354
+ if (!opt.raw && this.cfg.hooks!.afterLoad && rows.length) {
355
+ rows = (await pMap(rows, async dbm => await this.cfg.hooks!.afterLoad!(dbm))).filter(
356
+ _isTruthy,
357
+ )
358
+ }
359
+
328
360
  const bms = partialQuery || opt.raw ? (rows as any[]) : await this.dbmsToBM(rows, opt)
329
361
  this.logResult(started, op, bms, q.table)
330
362
  return {
@@ -345,7 +377,13 @@ export class CommonDao<
345
377
  q.table = opt.table || q.table
346
378
  const op = `runQueryAsDBM(${q.pretty()})`
347
379
  const started = this.logStarted(op, q.table)
348
- const { rows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
380
+ let { rows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
381
+ if (!opt.raw && this.cfg.hooks!.afterLoad && rows.length) {
382
+ rows = (await pMap(rows, async dbm => await this.cfg.hooks!.afterLoad!(dbm))).filter(
383
+ _isTruthy,
384
+ )
385
+ }
386
+
349
387
  const partialQuery = !!q._selectedFieldNames
350
388
  const dbms = partialQuery || opt.raw ? rows : this.anyToDBMs(rows, opt)
351
389
  this.logResult(started, op, dbms, q.table)
@@ -364,7 +402,13 @@ export class CommonDao<
364
402
  q.table = opt.table || q.table
365
403
  const op = `runQueryAsTM(${q.pretty()})`
366
404
  const started = this.logStarted(op, q.table)
367
- const { rows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
405
+ let { rows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
406
+ if (!opt.raw && this.cfg.hooks!.afterLoad && rows.length) {
407
+ rows = (await pMap(rows, async dbm => await this.cfg.hooks!.afterLoad!(dbm))).filter(
408
+ _isTruthy,
409
+ )
410
+ }
411
+
368
412
  const partialQuery = !!q._selectedFieldNames
369
413
  const tms =
370
414
  partialQuery || opt.raw ? (rows as any[]) : this.bmsToTM(await this.dbmsToBM(rows, opt), opt)
@@ -403,12 +447,17 @@ export class CommonDao<
403
447
 
404
448
  await _pipeline([
405
449
  this.cfg.db.streamQuery<DBM>(q, opt),
406
- // optimization: 1 validation is enough
407
- // transformMap<any, DBM>(dbm => (partialQuery || opt.raw ? dbm : this.anyToDBM(dbm, opt)), opt),
408
450
  transformMap<DBM, Saved<BM>>(
409
451
  async dbm => {
410
452
  count++
411
- return partialQuery || opt.raw ? (dbm as any) : await this.dbmToBM(dbm, opt)
453
+ if (partialQuery || opt.raw) return dbm as any
454
+
455
+ if (this.cfg.hooks!.afterLoad) {
456
+ dbm = (await this.cfg.hooks!.afterLoad(dbm)) as DBM
457
+ if (dbm === null) return SKIP
458
+ }
459
+
460
+ return await this.dbmToBM(dbm, opt)
412
461
  },
413
462
  {
414
463
  errorMode: opt.errorMode,
@@ -448,10 +497,17 @@ export class CommonDao<
448
497
 
449
498
  await _pipeline([
450
499
  this.cfg.db.streamQuery<any>(q, opt),
451
- transformMapSync<any, DBM>(
452
- dbm => {
500
+ transformMap<any, DBM>(
501
+ async dbm => {
453
502
  count++
454
- return partialQuery || opt.raw ? dbm : this.anyToDBM(dbm, opt)
503
+ if (partialQuery || opt.raw) return dbm
504
+
505
+ if (this.cfg.hooks!.afterLoad) {
506
+ dbm = (await this.cfg.hooks!.afterLoad(dbm)) as DBM
507
+ if (dbm === null) return SKIP
508
+ }
509
+
510
+ return this.anyToDBM(dbm, opt)
455
511
  },
456
512
  {
457
513
  errorMode: opt.errorMode,
@@ -491,9 +547,19 @@ export class CommonDao<
491
547
  return stream
492
548
  .on('error', err => stream.emit('error', err))
493
549
  .pipe(
494
- transformMapSimple<any, DBM>(dbm => this.anyToDBM(dbm, opt), {
495
- errorMode: ErrorMode.SUPPRESS, // cause .pipe() cannot propagate errors
496
- }),
550
+ transformMap<any, DBM>(
551
+ async dbm => {
552
+ if (this.cfg.hooks!.afterLoad) {
553
+ dbm = (await this.cfg.hooks!.afterLoad(dbm)) as DBM
554
+ if (dbm === null) return SKIP
555
+ }
556
+
557
+ return this.anyToDBM(dbm, opt)
558
+ },
559
+ {
560
+ errorMode: opt.errorMode,
561
+ },
562
+ ),
497
563
  )
498
564
  }
499
565
 
@@ -523,9 +589,19 @@ export class CommonDao<
523
589
  // .pipe(transformMap<DBM, Saved<BM>>(dbm => this.dbmToBM(dbm, opt), safeOpt))
524
590
  .on('error', err => stream.emit('error', err))
525
591
  .pipe(
526
- transformMap<DBM, Saved<BM>>(async dbm => await this.dbmToBM(dbm, opt), {
527
- errorMode: ErrorMode.SUPPRESS, // cause .pipe() cannot propagate errors
528
- }),
592
+ transformMap<DBM, Saved<BM>>(
593
+ async dbm => {
594
+ if (this.cfg.hooks!.afterLoad) {
595
+ dbm = (await this.cfg.hooks!.afterLoad(dbm)) as DBM
596
+ if (dbm === null) return SKIP
597
+ }
598
+
599
+ return await this.dbmToBM(dbm, opt)
600
+ },
601
+ {
602
+ errorMode: opt.errorMode,
603
+ },
604
+ ),
529
605
  )
530
606
  // this can make the stream async-iteration-friendly
531
607
  // but not applying it now for perf reasons
@@ -569,8 +645,10 @@ export class CommonDao<
569
645
 
570
646
  await _pipeline([
571
647
  this.cfg.db.streamQuery<DBM>(q.select(['id']), opt),
572
- transformMapSimple<DBM, ID>(objectWithId => objectWithId.id),
573
- transformTap(() => count++),
648
+ transformMapSimple<DBM, ID>(objectWithId => {
649
+ count++
650
+ return objectWithId.id
651
+ }),
574
652
  transformMap<ID, void>(mapper, {
575
653
  ...opt,
576
654
  predicate: _passthroughPredicate,
@@ -618,8 +696,10 @@ export class CommonDao<
618
696
  save: async (
619
697
  bm: Unsaved<BM>,
620
698
  opt: CommonDaoSaveOptions<DBM> = {},
621
- ): Promise<DBSaveBatchOperation> => {
622
- const row: DBM = (await this.save(bm, { ...opt, tx: true })) as any
699
+ ): Promise<DBSaveBatchOperation | undefined> => {
700
+ // .save actually returns DBM (not BM) when it detects `opt.tx === true`
701
+ const row: DBM | null = (await this.save(bm, { ...opt, tx: true })) as any
702
+ if (row === null) return
623
703
 
624
704
  return {
625
705
  type: 'saveBatch',
@@ -635,8 +715,8 @@ export class CommonDao<
635
715
  bms: Unsaved<BM>[],
636
716
  opt: CommonDaoSaveOptions<DBM> = {},
637
717
  ): Promise<DBSaveBatchOperation | undefined> => {
638
- if (!bms.length) return
639
718
  const rows: DBM[] = (await this.saveBatch(bms, { ...opt, tx: true })) as any
719
+ if (!rows.length) return
640
720
 
641
721
  return {
642
722
  type: 'saveBatch',
@@ -682,9 +762,15 @@ export class CommonDao<
682
762
  this.requireWriteAccess()
683
763
  const idWasGenerated = !bm.id && this.cfg.createId
684
764
  this.assignIdCreatedUpdated(bm, opt) // mutates
685
- const dbm = await this.bmToDBM(bm as BM, opt)
765
+ let dbm = await this.bmToDBM(bm as BM, opt)
766
+
767
+ if (this.cfg.hooks!.beforeSave) {
768
+ dbm = (await this.cfg.hooks!.beforeSave(dbm)) as DBM
769
+ if (dbm === null && !opt.tx) return bm as any
770
+ }
686
771
 
687
772
  if (opt.tx) {
773
+ // May return `null`, in which case it'll be skipped
688
774
  return dbm as any
689
775
  }
690
776
 
@@ -697,6 +783,7 @@ export class CommonDao<
697
783
  const started = this.logSaveStarted(op, bm, table)
698
784
  const { excludeFromIndexes } = this.cfg
699
785
  const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds
786
+
700
787
  await this.cfg.db.saveBatch(table, [dbm], {
701
788
  excludeFromIndexes,
702
789
  assignGeneratedIds,
@@ -763,6 +850,12 @@ export class CommonDao<
763
850
  const started = this.logSaveStarted(op, row, table)
764
851
  const { excludeFromIndexes } = this.cfg
765
852
  const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds
853
+
854
+ if (this.cfg.hooks!.beforeSave) {
855
+ row = (await this.cfg.hooks!.beforeSave(row)) as DBM
856
+ if (row === null) return dbm
857
+ }
858
+
766
859
  await this.cfg.db.saveBatch(table, [row], {
767
860
  excludeFromIndexes,
768
861
  assignGeneratedIds,
@@ -778,10 +871,17 @@ export class CommonDao<
778
871
  }
779
872
 
780
873
  async saveBatch(bms: Unsaved<BM>[], opt: CommonDaoSaveOptions<DBM> = {}): Promise<Saved<BM>[]> {
874
+ if (!bms.length) return []
781
875
  this.requireWriteAccess()
782
876
  const table = opt.table || this.cfg.table
783
877
  bms.forEach(bm => this.assignIdCreatedUpdated(bm, opt))
784
- const dbms = await this.bmsToDBM(bms as BM[], opt)
878
+ let dbms = await this.bmsToDBM(bms as BM[], opt)
879
+
880
+ if (this.cfg.hooks!.beforeSave && dbms.length) {
881
+ dbms = (await pMap(dbms, async dbm => await this.cfg.hooks!.beforeSave!(dbm))).filter(
882
+ _isTruthy,
883
+ )
884
+ }
785
885
 
786
886
  if (opt.tx) {
787
887
  return dbms as any
@@ -802,6 +902,7 @@ export class CommonDao<
802
902
  const started = this.logSaveStarted(op, bms, table)
803
903
  const { excludeFromIndexes } = this.cfg
804
904
  const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds
905
+
805
906
  await this.cfg.db.saveBatch(table, dbms, {
806
907
  excludeFromIndexes,
807
908
  assignGeneratedIds,
@@ -818,6 +919,7 @@ export class CommonDao<
818
919
  }
819
920
 
820
921
  async saveBatchAsDBM(dbms: DBM[], opt: CommonDaoSaveOptions<DBM> = {}): Promise<DBM[]> {
922
+ if (!dbms.length) return []
821
923
  this.requireWriteAccess()
822
924
  const table = opt.table || this.cfg.table
823
925
  let rows = dbms
@@ -840,6 +942,12 @@ export class CommonDao<
840
942
  const { excludeFromIndexes } = this.cfg
841
943
  const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds
842
944
 
945
+ if (this.cfg.hooks!.beforeSave && rows.length) {
946
+ rows = (await pMap(rows, async row => await this.cfg.hooks!.beforeSave!(row))).filter(
947
+ _isTruthy,
948
+ )
949
+ }
950
+
843
951
  await this.cfg.db.saveBatch(table, rows, {
844
952
  excludeFromIndexes,
845
953
  assignGeneratedIds,
@@ -873,6 +981,7 @@ export class CommonDao<
873
981
  }
874
982
 
875
983
  async deleteByIds(ids: ID[], opt: CommonDaoOptions = {}): Promise<number> {
984
+ if (!ids.length) return 0
876
985
  this.requireWriteAccess()
877
986
  this.requireObjectMutability(opt)
878
987
  const op = `deleteByIds(${ids.join(', ')})`
@@ -941,6 +1050,7 @@ export class CommonDao<
941
1050
  }
942
1051
 
943
1052
  async updateByIds(ids: ID[], patch: DBPatch<DBM>, opt: CommonDaoOptions = {}): Promise<number> {
1053
+ if (!ids.length) return 0
944
1054
  return await this.updateByQuery(this.query().filterIn('id', ids), patch, opt)
945
1055
  }
946
1056
 
@@ -1187,14 +1297,14 @@ export class CommonDao<
1187
1297
  this.cfg.logger?.log(`<< ${table}.${op} in ${_since(started)}`)
1188
1298
  }
1189
1299
 
1190
- protected logStarted(op: string, table: string, force = false): number {
1300
+ protected logStarted(op: string, table: string, force = false): UnixTimestampMillisNumber {
1191
1301
  if (this.cfg.logStarted || force) {
1192
1302
  this.cfg.logger?.log(`>> ${table}.${op}`)
1193
1303
  }
1194
1304
  return Date.now()
1195
1305
  }
1196
1306
 
1197
- protected logSaveStarted(op: string, items: any, table: string): number {
1307
+ protected logSaveStarted(op: string, items: any, table: string): UnixTimestampMillisNumber {
1198
1308
  if (this.cfg.logStarted) {
1199
1309
  const args: any[] = [`>> ${table}.${op}`]
1200
1310
  if (Array.isArray(items)) {
@@ -1,3 +1,5 @@
1
+ import * as fs from 'node:fs'
2
+ import * as fsp from 'node:fs/promises'
1
3
  import { createGzip, ZlibOptions } from 'node:zlib'
2
4
  import {
3
5
  AppError,
@@ -16,9 +18,12 @@ import {
16
18
  transformTap,
17
19
  transformToNDJson,
18
20
  _pipeline,
21
+ _ensureDirSync,
22
+ _pathExistsSync,
23
+ _ensureFileSync,
24
+ _writeJsonFile,
19
25
  } from '@naturalcycles/nodejs-lib'
20
26
  import { boldWhite, dimWhite, grey, yellow } from '@naturalcycles/nodejs-lib/dist/colors'
21
- import * as fs from 'fs-extra'
22
27
  import { CommonDB } from '../common.db'
23
28
  import { DBQuery } from '../index'
24
29
 
@@ -156,7 +161,7 @@ export async function dbPipelineBackup(opt: DBPipelineBackupOptions): Promise<ND
156
161
  `>> ${dimWhite('dbPipelineBackup')} started in ${grey(outputDirPath)}...${sinceUpdatedStr}`,
157
162
  )
158
163
 
159
- fs.ensureDirSync(outputDirPath)
164
+ _ensureDirSync(outputDirPath)
160
165
 
161
166
  tables ||= await db.getTables()
162
167
 
@@ -176,20 +181,20 @@ export async function dbPipelineBackup(opt: DBPipelineBackupOptions): Promise<ND
176
181
  const filePath = `${outputDirPath}/${table}.ndjson` + (gzip ? '.gz' : '')
177
182
  const schemaFilePath = `${outputDirPath}/${table}.schema.json`
178
183
 
179
- if (protectFromOverwrite && (await fs.pathExists(filePath))) {
184
+ if (protectFromOverwrite && _pathExistsSync(filePath)) {
180
185
  throw new AppError(`dbPipelineBackup: output file exists: ${filePath}`)
181
186
  }
182
187
 
183
188
  const started = Date.now()
184
189
  let rows = 0
185
190
 
186
- await fs.ensureFile(filePath)
191
+ _ensureFileSync(filePath)
187
192
 
188
193
  console.log(`>> ${grey(filePath)} started...`)
189
194
 
190
195
  if (emitSchemaFromDB) {
191
196
  const schema = await db.getTableSchema(table)
192
- await fs.writeJson(schemaFilePath, schema, { spaces: 2 })
197
+ await _writeJsonFile(schemaFilePath, schema, { spaces: 2 })
193
198
  console.log(`>> ${grey(schemaFilePath)} saved (generated from DB)`)
194
199
  }
195
200
 
@@ -214,7 +219,7 @@ export async function dbPipelineBackup(opt: DBPipelineBackupOptions): Promise<ND
214
219
  fs.createWriteStream(filePath),
215
220
  ])
216
221
 
217
- const { size: sizeBytes } = await fs.stat(filePath)
222
+ const { size: sizeBytes } = await fsp.stat(filePath)
218
223
 
219
224
  const stats = NDJsonStats.create({
220
225
  tookMillis: Date.now() - started,
@@ -1,3 +1,4 @@
1
+ import * as fs from 'node:fs'
1
2
  import { createUnzip } from 'node:zlib'
2
3
  import {
3
4
  AsyncMapper,
@@ -8,6 +9,7 @@ import {
8
9
  _passthroughMapper,
9
10
  SavedDBEntity,
10
11
  localTime,
12
+ JsonSchemaObject,
11
13
  } from '@naturalcycles/js-lib'
12
14
  import {
13
15
  NDJsonStats,
@@ -23,9 +25,10 @@ import {
23
25
  transformTap,
24
26
  writableForEach,
25
27
  _pipeline,
28
+ _ensureDirSync,
29
+ _readJsonFile,
26
30
  } from '@naturalcycles/nodejs-lib'
27
31
  import { boldWhite, dimWhite, grey, yellow } from '@naturalcycles/nodejs-lib/dist/colors'
28
- import * as fs from 'fs-extra'
29
32
  import { CommonDB } from '../common.db'
30
33
  import { CommonDBSaveOptions } from '../index'
31
34
 
@@ -139,7 +142,7 @@ export async function dbPipelineRestore(opt: DBPipelineRestoreOptions): Promise<
139
142
  `>> ${dimWhite('dbPipelineRestore')} started in ${grey(inputDirPath)}...${sinceUpdatedStr}`,
140
143
  )
141
144
 
142
- fs.ensureDirSync(inputDirPath)
145
+ _ensureDirSync(inputDirPath)
143
146
 
144
147
  const tablesToGzip = new Set<string>()
145
148
  const sizeByTable: Record<string, number> = {}
@@ -179,7 +182,7 @@ export async function dbPipelineRestore(opt: DBPipelineRestoreOptions): Promise<
179
182
  return
180
183
  }
181
184
 
182
- const schema = await fs.readJson(schemaFilePath)
185
+ const schema = await _readJsonFile<JsonSchemaObject<any>>(schemaFilePath)
183
186
  await db.createTable(table, schema, { dropIfExists: true })
184
187
  })
185
188
  }