@naturalcycles/db-lib 10.18.0 → 10.20.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.
@@ -1,19 +1,18 @@
1
1
  import { _isTruthy } from '@naturalcycles/js-lib';
2
2
  import { _uniqBy } from '@naturalcycles/js-lib/array/array.util.js';
3
3
  import { localTime } from '@naturalcycles/js-lib/datetime/localTime.js';
4
- import { _since } from '@naturalcycles/js-lib/datetime/time.util.js';
5
- import { _assert, AppError, ErrorMode } from '@naturalcycles/js-lib/error';
4
+ import { _assert, ErrorMode } from '@naturalcycles/js-lib/error';
6
5
  import { _deepJsonEquals } from '@naturalcycles/js-lib/object/deepEquals.js';
7
- import { _deepCopy, _filterUndefinedValues, _objectAssignExact, } from '@naturalcycles/js-lib/object/object.util.js';
6
+ import { _filterUndefinedValues, _objectAssignExact, } from '@naturalcycles/js-lib/object/object.util.js';
8
7
  import { pMap } from '@naturalcycles/js-lib/promise/pMap.js';
9
- import { _truncate } from '@naturalcycles/js-lib/string/string.util.js';
8
+ import { _stringMapEntries, _stringMapValues, } from '@naturalcycles/js-lib/types';
10
9
  import { _passthroughPredicate, _typeCast, SKIP } from '@naturalcycles/js-lib/types';
11
10
  import { stringId } from '@naturalcycles/nodejs-lib';
12
- import { transformFlatten } from '@naturalcycles/nodejs-lib/stream';
11
+ import { transformFlatten, transformMapSync, } from '@naturalcycles/nodejs-lib/stream';
13
12
  import { _pipeline, transformChunk, transformLogProgress, transformMap, transformNoOp, writableVoid, } from '@naturalcycles/nodejs-lib/stream';
14
13
  import { DBLibError } from '../cnst.js';
15
14
  import { RunnableDBQuery } from '../query/dbQuery.js';
16
- import { CommonDaoLogLevel } from './common.dao.model.js';
15
+ import { CommonDaoTransaction } from './commonDaoTransaction.js';
17
16
  /**
18
17
  * Lowest common denominator API between supported Databases.
19
18
  *
@@ -26,7 +25,6 @@ export class CommonDao {
26
25
  constructor(cfg) {
27
26
  this.cfg = cfg;
28
27
  this.cfg = {
29
- logLevel: CommonDaoLogLevel.NONE,
30
28
  generateId: true,
31
29
  assignGeneratedIds: false,
32
30
  useCreatedProperty: true,
@@ -58,22 +56,13 @@ export class CommonDao {
58
56
  return this.validateAndConvert(bm, undefined, opt);
59
57
  }
60
58
  // GET
61
- // overrides are disabled now, as they obfuscate errors when ID branded type is used
62
- // async getById(id: undefined | null, opt?: CommonDaoOptions): Promise<null>
63
- // async getById(id?: ID | null, opt?: CommonDaoOptions): Promise<BM | null>
64
- async getById(id, opt = {}) {
65
- if (!id)
66
- return null;
67
- const op = `getById(${id})`;
68
- const table = opt.table || this.cfg.table;
69
- const started = this.logStarted(op, table);
70
- let dbm = (await (opt.tx || this.cfg.db).getByIds(table, [id], opt))[0];
71
- if (dbm && this.cfg.hooks.afterLoad) {
72
- dbm = (await this.cfg.hooks.afterLoad(dbm)) || undefined;
73
- }
74
- const bm = await this.dbmToBM(dbm, opt);
75
- this.logResult(started, op, bm, table);
76
- return bm || null;
59
+ async requireById(id, opt = {}) {
60
+ const bm = await this.getById(id, opt);
61
+ return this.ensureRequired(bm, id, opt);
62
+ }
63
+ async requireByIdAsDBM(id, opt = {}) {
64
+ const dbm = await this.getByIdAsDBM(id, opt);
65
+ return this.ensureRequired(dbm, id, opt);
77
66
  }
78
67
  async getByIdOrEmpty(id, part = {}, opt) {
79
68
  const bm = await this.getById(id, opt);
@@ -81,99 +70,32 @@ export class CommonDao {
81
70
  return bm;
82
71
  return this.create({ ...part, id }, opt);
83
72
  }
84
- // async getByIdAsDBM(id: undefined | null, opt?: CommonDaoOptions): Promise<null>
85
- // async getByIdAsDBM(id?: ID | null, opt?: CommonDaoOptions): Promise<DBM | null>
73
+ async getById(id, opt = {}) {
74
+ if (!id)
75
+ return null;
76
+ const [dbm] = await this.loadByIds([id], opt);
77
+ return await this.dbmToBM(dbm, opt);
78
+ }
86
79
  async getByIdAsDBM(id, opt = {}) {
87
80
  if (!id)
88
81
  return null;
89
- const op = `getByIdAsDBM(${id})`;
90
- const table = opt.table || this.cfg.table;
91
- const started = this.logStarted(op, table);
92
- let [dbm] = await (opt.tx || this.cfg.db).getByIds(table, [id], opt);
93
- if (dbm && this.cfg.hooks.afterLoad) {
94
- dbm = (await this.cfg.hooks.afterLoad(dbm)) || undefined;
95
- }
96
- dbm = this.anyToDBM(dbm, opt);
97
- this.logResult(started, op, dbm, table);
98
- return dbm || null;
82
+ const [row] = await this.loadByIds([id], opt);
83
+ return this.anyToDBM(row, opt) || null;
99
84
  }
100
85
  async getByIds(ids, opt = {}) {
101
- if (!ids.length)
102
- return [];
103
- const op = `getByIds ${ids.length} id(s) (${_truncate(ids.slice(0, 10).join(', '), 50)})`;
104
- const table = opt.table || this.cfg.table;
105
- const started = this.logStarted(op, table);
106
- let dbms = await (opt.tx || this.cfg.db).getByIds(table, ids, opt);
107
- if (this.cfg.hooks.afterLoad && dbms.length) {
108
- dbms = (await pMap(dbms, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(_isTruthy);
109
- }
110
- const bms = await this.dbmsToBM(dbms, opt);
111
- this.logResult(started, op, bms, table);
112
- return bms;
86
+ const dbms = await this.loadByIds(ids, opt);
87
+ return await this.dbmsToBM(dbms, opt);
113
88
  }
114
89
  async getByIdsAsDBM(ids, opt = {}) {
90
+ const rows = await this.loadByIds(ids, opt);
91
+ return this.anyToDBMs(rows);
92
+ }
93
+ // DRY private method
94
+ async loadByIds(ids, opt = {}) {
115
95
  if (!ids.length)
116
96
  return [];
117
- const op = `getByIdsAsDBM ${ids.length} id(s) (${_truncate(ids.slice(0, 10).join(', '), 50)})`;
118
97
  const table = opt.table || this.cfg.table;
119
- const started = this.logStarted(op, table);
120
- let dbms = await (opt.tx || this.cfg.db).getByIds(table, ids, opt);
121
- if (this.cfg.hooks.afterLoad && dbms.length) {
122
- dbms = (await pMap(dbms, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(_isTruthy);
123
- }
124
- this.logResult(started, op, dbms, table);
125
- return dbms;
126
- }
127
- async requireById(id, opt = {}) {
128
- const r = await this.getById(id, opt);
129
- if (!r) {
130
- this.throwRequiredError(id, opt);
131
- }
132
- return r;
133
- }
134
- async requireByIdAsDBM(id, opt = {}) {
135
- const r = await this.getByIdAsDBM(id, opt);
136
- if (!r) {
137
- this.throwRequiredError(id, opt);
138
- }
139
- return r;
140
- }
141
- throwRequiredError(id, opt) {
142
- const table = opt.table || this.cfg.table;
143
- throw new AppError(`DB row required, but not found in ${table}`, {
144
- table,
145
- id,
146
- });
147
- }
148
- /**
149
- * Throws if readOnly is true
150
- */
151
- requireWriteAccess() {
152
- if (this.cfg.readOnly) {
153
- throw new AppError(DBLibError.DAO_IS_READ_ONLY, {
154
- table: this.cfg.table,
155
- });
156
- }
157
- }
158
- /**
159
- * Throws if readOnly is true
160
- */
161
- requireObjectMutability(opt) {
162
- if (this.cfg.immutable && !opt.allowMutability) {
163
- throw new AppError(DBLibError.OBJECT_IS_IMMUTABLE, {
164
- table: this.cfg.table,
165
- });
166
- }
167
- }
168
- async ensureUniqueId(table, dbm) {
169
- // todo: retry N times
170
- const existing = await this.cfg.db.getByIds(table, [dbm.id]);
171
- if (existing.length) {
172
- throw new AppError(DBLibError.NON_UNIQUE_ID, {
173
- table,
174
- ids: existing.map(i => i.id),
175
- });
176
- }
98
+ return await (opt.tx || this.cfg.db).getByIds(table, ids, opt);
177
99
  }
178
100
  async getBy(by, value, limit = 0, opt) {
179
101
  return await this.query().filterEq(by, value).limit(limit).runQuery(opt);
@@ -214,15 +136,9 @@ export class CommonDao {
214
136
  async runQueryExtended(q, opt = {}) {
215
137
  this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
216
138
  q.table = opt.table || q.table;
217
- const op = `runQuery(${q.pretty()})`;
218
- const started = this.logStarted(op, q.table);
219
- let { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
220
- const partialQuery = !!q._selectedFieldNames;
221
- if (this.cfg.hooks.afterLoad && rows.length) {
222
- rows = (await pMap(rows, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(_isTruthy);
223
- }
224
- const bms = partialQuery ? rows : await this.dbmsToBM(rows, opt);
225
- this.logResult(started, op, bms, q.table);
139
+ const { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
140
+ const isPartialQuery = !!q._selectedFieldNames;
141
+ const bms = isPartialQuery ? rows : await this.dbmsToBM(rows, opt);
226
142
  return {
227
143
  rows: bms,
228
144
  ...queryResult,
@@ -235,48 +151,27 @@ export class CommonDao {
235
151
  async runQueryExtendedAsDBM(q, opt = {}) {
236
152
  this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
237
153
  q.table = opt.table || q.table;
238
- const op = `runQueryAsDBM(${q.pretty()})`;
239
- const started = this.logStarted(op, q.table);
240
- let { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
241
- if (this.cfg.hooks.afterLoad && rows.length) {
242
- rows = (await pMap(rows, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(_isTruthy);
243
- }
244
- const partialQuery = !!q._selectedFieldNames;
245
- const dbms = partialQuery ? rows : this.anyToDBMs(rows, opt);
246
- this.logResult(started, op, dbms, q.table);
154
+ const { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
155
+ const isPartialQuery = !!q._selectedFieldNames;
156
+ const dbms = isPartialQuery ? rows : this.anyToDBMs(rows, opt);
247
157
  return { rows: dbms, ...queryResult };
248
158
  }
249
159
  async runQueryCount(q, opt = {}) {
250
160
  this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
251
161
  q.table = opt.table || q.table;
252
- const op = `runQueryCount(${q.pretty()})`;
253
- const started = this.logStarted(op, q.table);
254
- const count = await this.cfg.db.runQueryCount(q, opt);
255
- if (this.cfg.logLevel >= CommonDaoLogLevel.OPERATIONS) {
256
- this.cfg.logger?.log(`<< ${q.table}.${op}: ${count} row(s) in ${_since(started)}`);
257
- }
258
- return count;
162
+ return await this.cfg.db.runQueryCount(q, opt);
259
163
  }
260
164
  async streamQueryForEach(q, mapper, opt = {}) {
261
165
  this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
262
166
  q.table = opt.table || q.table;
263
167
  opt.skipValidation = opt.skipValidation !== false; // default true
264
168
  opt.errorMode ||= ErrorMode.SUPPRESS;
265
- const partialQuery = !!q._selectedFieldNames;
266
- const op = `streamQueryForEach(${q.pretty()})`;
267
- const started = this.logStarted(op, q.table, true);
268
- let count = 0;
169
+ const isPartialQuery = !!q._selectedFieldNames;
269
170
  await _pipeline([
270
171
  this.cfg.db.streamQuery(q, opt),
271
172
  transformMap(async (dbm) => {
272
- count++;
273
- if (partialQuery)
173
+ if (isPartialQuery)
274
174
  return dbm;
275
- if (this.cfg.hooks.afterLoad) {
276
- dbm = (await this.cfg.hooks.afterLoad(dbm));
277
- if (dbm === null)
278
- return SKIP;
279
- }
280
175
  return await this.dbmToBM(dbm, opt);
281
176
  }, {
282
177
  errorMode: opt.errorMode,
@@ -292,30 +187,18 @@ export class CommonDao {
292
187
  }),
293
188
  writableVoid(),
294
189
  ]);
295
- if (this.cfg.logLevel >= CommonDaoLogLevel.OPERATIONS) {
296
- this.cfg.logger?.log(`<< ${q.table}.${op}: ${count} row(s) in ${_since(started)}`);
297
- }
298
190
  }
299
191
  async streamQueryAsDBMForEach(q, mapper, opt = {}) {
300
192
  this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
301
193
  q.table = opt.table || q.table;
302
194
  opt.skipValidation = opt.skipValidation !== false; // default true
303
195
  opt.errorMode ||= ErrorMode.SUPPRESS;
304
- const partialQuery = !!q._selectedFieldNames;
305
- const op = `streamQueryAsDBMForEach(${q.pretty()})`;
306
- const started = this.logStarted(op, q.table, true);
307
- let count = 0;
196
+ const isPartialQuery = !!q._selectedFieldNames;
308
197
  await _pipeline([
309
198
  this.cfg.db.streamQuery(q, opt),
310
- transformMap(async (dbm) => {
311
- count++;
312
- if (partialQuery)
199
+ transformMapSync(dbm => {
200
+ if (isPartialQuery)
313
201
  return dbm;
314
- if (this.cfg.hooks.afterLoad) {
315
- dbm = (await this.cfg.hooks.afterLoad(dbm));
316
- if (dbm === null)
317
- return SKIP;
318
- }
319
202
  return this.anyToDBM(dbm, opt);
320
203
  }, {
321
204
  errorMode: opt.errorMode,
@@ -331,9 +214,6 @@ export class CommonDao {
331
214
  }),
332
215
  writableVoid(),
333
216
  ]);
334
- if (this.cfg.logLevel >= CommonDaoLogLevel.OPERATIONS) {
335
- this.cfg.logger?.log(`<< ${q.table}.${op}: ${count} row(s) in ${_since(started)}`);
336
- }
337
217
  }
338
218
  /**
339
219
  * Stream as Readable, to be able to .pipe() it further with support of backpressure.
@@ -343,19 +223,14 @@ export class CommonDao {
343
223
  q.table = opt.table || q.table;
344
224
  opt.skipValidation = opt.skipValidation !== false; // default true
345
225
  opt.errorMode ||= ErrorMode.SUPPRESS;
346
- const partialQuery = !!q._selectedFieldNames;
226
+ const isPartialQuery = !!q._selectedFieldNames;
347
227
  const stream = this.cfg.db.streamQuery(q, opt);
348
- if (partialQuery)
228
+ if (isPartialQuery)
349
229
  return stream;
350
230
  return (stream
351
231
  // the commented out line was causing RangeError: Maximum call stack size exceeded
352
232
  // .on('error', err => stream.emit('error', err))
353
- .pipe(transformMap(async (dbm) => {
354
- if (this.cfg.hooks.afterLoad) {
355
- dbm = (await this.cfg.hooks.afterLoad(dbm));
356
- if (dbm === null)
357
- return SKIP;
358
- }
233
+ .pipe(transformMapSync(dbm => {
359
234
  return this.anyToDBM(dbm, opt);
360
235
  }, {
361
236
  errorMode: opt.errorMode,
@@ -376,16 +251,11 @@ export class CommonDao {
376
251
  opt.skipValidation = opt.skipValidation !== false; // default true
377
252
  opt.errorMode ||= ErrorMode.SUPPRESS;
378
253
  const stream = this.cfg.db.streamQuery(q, opt);
379
- const partialQuery = !!q._selectedFieldNames;
380
- if (partialQuery)
254
+ const isPartialQuery = !!q._selectedFieldNames;
255
+ if (isPartialQuery)
381
256
  return stream;
382
257
  // This almost works, but hard to implement `errorMode: THROW_AGGREGATED` in this case
383
258
  // return stream.flatMap(async (dbm: DBM) => {
384
- // if (this.cfg.hooks!.afterLoad) {
385
- // dbm = (await this.cfg.hooks!.afterLoad(dbm))!
386
- // if (dbm === null) return [] // SKIP
387
- // }
388
- //
389
259
  // return [await this.dbmToBM(dbm, opt)] satisfies BM[]
390
260
  // }, {
391
261
  // concurrency: 16,
@@ -397,11 +267,6 @@ export class CommonDao {
397
267
  // the commented out line was causing RangeError: Maximum call stack size exceeded
398
268
  // .on('error', err => stream.emit('error', err))
399
269
  .pipe(transformMap(async (dbm) => {
400
- if (this.cfg.hooks.afterLoad) {
401
- dbm = (await this.cfg.hooks.afterLoad(dbm));
402
- if (dbm === null)
403
- return SKIP;
404
- }
405
270
  return await this.dbmToBM(dbm, opt);
406
271
  }, {
407
272
  errorMode: opt.errorMode,
@@ -441,14 +306,8 @@ export class CommonDao {
441
306
  this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
442
307
  q.table = opt.table || q.table;
443
308
  opt.errorMode ||= ErrorMode.SUPPRESS;
444
- const op = `streamQueryIdsForEach(${q.pretty()})`;
445
- const started = this.logStarted(op, q.table, true);
446
- let count = 0;
447
309
  await _pipeline([
448
- this.cfg.db.streamQuery(q.select(['id']), opt).map(r => {
449
- count++;
450
- return r.id;
451
- }),
310
+ this.cfg.db.streamQuery(q.select(['id']), opt).map(r => r.id),
452
311
  transformMap(mapper, {
453
312
  ...opt,
454
313
  predicate: _passthroughPredicate,
@@ -460,13 +319,9 @@ export class CommonDao {
460
319
  }),
461
320
  writableVoid(),
462
321
  ]);
463
- if (this.cfg.logLevel >= CommonDaoLogLevel.OPERATIONS) {
464
- this.cfg.logger?.log(`<< ${q.table}.${op}: ${count} id(s) in ${_since(started)}`);
465
- }
466
322
  }
467
323
  /**
468
324
  * Mutates!
469
- * "Returns", just to have a type of "Saved"
470
325
  */
471
326
  assignIdCreatedUpdated(obj, opt = {}) {
472
327
  const now = localTime.nowUnix();
@@ -480,7 +335,6 @@ export class CommonDao {
480
335
  obj.id ||= (this.cfg.hooks.createNaturalId?.(obj) ||
481
336
  this.cfg.hooks.createRandomId());
482
337
  }
483
- return obj;
484
338
  }
485
339
  // SAVE
486
340
  /**
@@ -598,7 +452,6 @@ export class CommonDao {
598
452
  return bm;
599
453
  }
600
454
  }
601
- const idWasGenerated = !bm.id && this.cfg.generateId;
602
455
  this.assignIdCreatedUpdated(bm, opt); // mutates
603
456
  _typeCast(bm);
604
457
  let dbm = await this.bmToDBM(bm, opt); // validates BM
@@ -608,125 +461,76 @@ export class CommonDao {
608
461
  return bm;
609
462
  }
610
463
  const table = opt.table || this.cfg.table;
611
- if (opt.ensureUniqueId && idWasGenerated)
612
- await this.ensureUniqueId(table, dbm);
613
- if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
614
- opt = { ...opt, saveMethod: 'insert' };
615
- }
616
- const op = `save(${dbm.id})`;
617
- const started = this.logSaveStarted(op, bm, table);
618
- const { excludeFromIndexes } = this.cfg;
619
- const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds;
620
- await (opt.tx || this.cfg.db).saveBatch(table, [dbm], {
621
- excludeFromIndexes,
622
- assignGeneratedIds,
623
- ...opt,
624
- });
625
- if (assignGeneratedIds) {
464
+ const saveOptions = this.prepareSaveOptions(opt);
465
+ await (opt.tx || this.cfg.db).saveBatch(table, [dbm], saveOptions);
466
+ if (saveOptions.assignGeneratedIds) {
626
467
  bm.id = dbm.id;
627
468
  }
628
- this.logSaveResult(started, op, table);
629
469
  return bm;
630
470
  }
631
471
  async saveAsDBM(dbm, opt = {}) {
632
472
  this.requireWriteAccess();
633
- const table = opt.table || this.cfg.table;
634
- // assigning id in case it misses the id
635
- // will override/set `updated` field, unless opts.preserveUpdated is set
636
- const idWasGenerated = !dbm.id && this.cfg.generateId;
637
473
  this.assignIdCreatedUpdated(dbm, opt); // mutates
638
474
  let row = this.anyToDBM(dbm, opt);
639
- if (opt.ensureUniqueId && idWasGenerated)
640
- await this.ensureUniqueId(table, row);
641
- if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
642
- opt = { ...opt, saveMethod: 'insert' };
643
- }
644
- const op = `saveAsDBM(${row.id})`;
645
- const started = this.logSaveStarted(op, row, table);
646
- const { excludeFromIndexes } = this.cfg;
647
- const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds;
648
475
  if (this.cfg.hooks.beforeSave) {
649
476
  row = (await this.cfg.hooks.beforeSave(row));
650
477
  if (row === null)
651
478
  return dbm;
652
479
  }
653
- await (opt.tx || this.cfg.db).saveBatch(table, [row], {
654
- excludeFromIndexes,
655
- assignGeneratedIds,
656
- ...opt,
657
- });
658
- if (assignGeneratedIds) {
480
+ const table = opt.table || this.cfg.table;
481
+ const saveOptions = this.prepareSaveOptions(opt);
482
+ await (opt.tx || this.cfg.db).saveBatch(table, [row], saveOptions);
483
+ if (saveOptions.assignGeneratedIds) {
659
484
  dbm.id = row.id;
660
485
  }
661
- this.logSaveResult(started, op, table);
662
486
  return row;
663
487
  }
664
488
  async saveBatch(bms, opt = {}) {
665
489
  if (!bms.length)
666
490
  return [];
667
491
  this.requireWriteAccess();
668
- const table = opt.table || this.cfg.table;
669
492
  bms.forEach(bm => this.assignIdCreatedUpdated(bm, opt));
670
493
  let dbms = await this.bmsToDBM(bms, opt);
671
494
  if (this.cfg.hooks.beforeSave && dbms.length) {
672
495
  dbms = (await pMap(dbms, async (dbm) => await this.cfg.hooks.beforeSave(dbm))).filter(_isTruthy);
673
496
  }
674
- if (opt.ensureUniqueId)
675
- throw new AppError('ensureUniqueId is not supported in saveBatch');
676
- if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
677
- opt = { ...opt, saveMethod: 'insert' };
678
- }
679
- const op = `saveBatch ${dbms.length} row(s) (${_truncate(dbms
680
- .slice(0, 10)
681
- .map(bm => bm.id)
682
- .join(', '), 50)})`;
683
- const started = this.logSaveStarted(op, bms, table);
684
- const { excludeFromIndexes } = this.cfg;
685
- const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds;
686
- await (opt.tx || this.cfg.db).saveBatch(table, dbms, {
687
- excludeFromIndexes,
688
- assignGeneratedIds,
689
- ...opt,
690
- });
691
- if (assignGeneratedIds) {
497
+ const table = opt.table || this.cfg.table;
498
+ const saveOptions = this.prepareSaveOptions(opt);
499
+ await (opt.tx || this.cfg.db).saveBatch(table, dbms, saveOptions);
500
+ if (saveOptions.assignGeneratedIds) {
692
501
  dbms.forEach((dbm, i) => (bms[i].id = dbm.id));
693
502
  }
694
- this.logSaveResult(started, op, table);
695
503
  return bms;
696
504
  }
697
505
  async saveBatchAsDBM(dbms, opt = {}) {
698
506
  if (!dbms.length)
699
507
  return [];
700
508
  this.requireWriteAccess();
701
- const table = opt.table || this.cfg.table;
702
- dbms.forEach(dbm => this.assignIdCreatedUpdated(dbm, opt)); // mutates
509
+ dbms.forEach(dbm => this.assignIdCreatedUpdated(dbm, opt));
703
510
  let rows = this.anyToDBMs(dbms, opt);
704
- if (opt.ensureUniqueId)
705
- throw new AppError('ensureUniqueId is not supported in saveBatch');
706
- if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
707
- opt = { ...opt, saveMethod: 'insert' };
708
- }
709
- const op = `saveBatchAsDBM ${rows.length} row(s) (${_truncate(rows
710
- .slice(0, 10)
711
- .map(bm => bm.id)
712
- .join(', '), 50)})`;
713
- const started = this.logSaveStarted(op, rows, table);
714
- const { excludeFromIndexes } = this.cfg;
715
- const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds;
716
511
  if (this.cfg.hooks.beforeSave && rows.length) {
717
512
  rows = (await pMap(rows, async (row) => await this.cfg.hooks.beforeSave(row))).filter(_isTruthy);
718
513
  }
719
- await (opt.tx || this.cfg.db).saveBatch(table, rows, {
720
- excludeFromIndexes,
721
- assignGeneratedIds,
722
- ...opt,
723
- });
724
- if (assignGeneratedIds) {
514
+ const table = opt.table || this.cfg.table;
515
+ const saveOptions = this.prepareSaveOptions(opt);
516
+ await (opt.tx || this.cfg.db).saveBatch(table, rows, saveOptions);
517
+ if (saveOptions.assignGeneratedIds) {
725
518
  rows.forEach((row, i) => (dbms[i].id = row.id));
726
519
  }
727
- this.logSaveResult(started, op, table);
728
520
  return rows;
729
521
  }
522
+ prepareSaveOptions(opt) {
523
+ let { saveMethod, assignGeneratedIds = this.cfg.assignGeneratedIds, excludeFromIndexes = this.cfg.excludeFromIndexes, } = opt;
524
+ if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
525
+ saveMethod = 'insert';
526
+ }
527
+ return {
528
+ ...opt,
529
+ excludeFromIndexes,
530
+ saveMethod,
531
+ assignGeneratedIds,
532
+ };
533
+ }
730
534
  /**
731
535
  * "Streaming" is implemented by buffering incoming rows into **batches**
732
536
  * (of size opt.chunkSize, which defaults to 500),
@@ -792,12 +596,8 @@ export class CommonDao {
792
596
  return 0;
793
597
  this.requireWriteAccess();
794
598
  this.requireObjectMutability(opt);
795
- const op = `deleteByIds(${ids.join(', ')})`;
796
599
  const table = opt.table || this.cfg.table;
797
- const started = this.logStarted(op, table);
798
- const count = await (opt.tx || this.cfg.db).deleteByIds(table, ids, opt);
799
- this.logSaveResult(started, op, table);
800
- return count;
600
+ return await (opt.tx || this.cfg.db).deleteByIds(table, ids, opt);
801
601
  }
802
602
  /**
803
603
  * Pass `chunkSize: number` (e.g 500) option to use Streaming: it will Stream the query, chunk by 500, and execute
@@ -809,8 +609,6 @@ export class CommonDao {
809
609
  this.requireWriteAccess();
810
610
  this.requireObjectMutability(opt);
811
611
  q.table = opt.table || q.table;
812
- const op = `deleteByQuery(${q.pretty()})`;
813
- const started = this.logStarted(op, q.table);
814
612
  let deleted = 0;
815
613
  if (opt.chunkSize) {
816
614
  const { chunkSize, chunkConcurrency = 8 } = opt;
@@ -838,7 +636,6 @@ export class CommonDao {
838
636
  else {
839
637
  deleted = await this.cfg.db.deleteByQuery(q, opt);
840
638
  }
841
- this.logSaveResult(started, op, q.table);
842
639
  return deleted;
843
640
  }
844
641
  async patchByIds(ids, patch, opt = {}) {
@@ -851,11 +648,7 @@ export class CommonDao {
851
648
  this.requireWriteAccess();
852
649
  this.requireObjectMutability(opt);
853
650
  q.table = opt.table || q.table;
854
- const op = `patchByQuery(${q.pretty()})`;
855
- const started = this.logStarted(op, q.table);
856
- const updated = await this.cfg.db.patchByQuery(q, patch, opt);
857
- this.logSaveResult(started, op, q.table);
858
- return updated;
651
+ return await this.cfg.db.patchByQuery(q, patch, opt);
859
652
  }
860
653
  /**
861
654
  * Caveat: it doesn't update created/updated props.
@@ -866,12 +659,9 @@ export class CommonDao {
866
659
  this.requireWriteAccess();
867
660
  this.requireObjectMutability(opt);
868
661
  const { table } = this.cfg;
869
- const op = `increment`;
870
- const started = this.logStarted(op, table);
871
662
  const result = await this.cfg.db.incrementBatch(table, prop, {
872
663
  [id]: by,
873
664
  });
874
- this.logSaveResult(started, op, table);
875
665
  return result[id];
876
666
  }
877
667
  /**
@@ -883,15 +673,11 @@ export class CommonDao {
883
673
  this.requireWriteAccess();
884
674
  this.requireObjectMutability(opt);
885
675
  const { table } = this.cfg;
886
- const op = `incrementBatch`;
887
- const started = this.logStarted(op, table);
888
- const result = await this.cfg.db.incrementBatch(table, prop, incrementMap);
889
- this.logSaveResult(started, op, table);
890
- return result;
676
+ return await this.cfg.db.incrementBatch(table, prop, incrementMap);
891
677
  }
892
678
  async dbmToBM(_dbm, opt = {}) {
893
679
  if (!_dbm)
894
- return;
680
+ return null;
895
681
  // optimization: no need to run full joi DBM validation, cause BM validation will be run
896
682
  // const dbm = this.anyToDBM(_dbm, opt)
897
683
  let dbm = { ..._dbm, ...this.cfg.hooks.parseNaturalId(_dbm.id) };
@@ -908,7 +694,7 @@ export class CommonDao {
908
694
  }
909
695
  async bmToDBM(bm, opt) {
910
696
  if (bm === undefined)
911
- return;
697
+ return null;
912
698
  // bm gets assigned to the new reference
913
699
  bm = this.validateAndConvert(bm, 'save', opt);
914
700
  // BM > DBM
@@ -920,7 +706,7 @@ export class CommonDao {
920
706
  }
921
707
  anyToDBM(dbm, opt = {}) {
922
708
  if (!dbm)
923
- return;
709
+ return null;
924
710
  // this shouldn't be happening on load! but should on save!
925
711
  // this.assignIdCreatedUpdated(dbm, opt)
926
712
  dbm = { ...dbm, ...this.cfg.hooks.parseNaturalId(dbm.id) };
@@ -933,8 +719,8 @@ export class CommonDao {
933
719
  // return this.validateAndConvert(dbm, this.cfg.dbmSchema, DBModelType.DBM, opt)
934
720
  return dbm;
935
721
  }
936
- anyToDBMs(entities, opt = {}) {
937
- return entities.map(entity => this.anyToDBM(entity, opt));
722
+ anyToDBMs(rows, opt = {}) {
723
+ return rows.map(entity => this.anyToDBM(entity, opt));
938
724
  }
939
725
  /**
940
726
  * Returns *converted value* (NOT the same reference).
@@ -981,63 +767,103 @@ export class CommonDao {
981
767
  async ping() {
982
768
  await this.cfg.db.ping();
983
769
  }
984
- id(id) {
770
+ withId(id) {
985
771
  return {
986
772
  dao: this,
987
773
  id,
988
774
  };
989
775
  }
990
- ids(ids) {
776
+ withIds(ids) {
991
777
  return {
992
778
  dao: this,
993
779
  ids,
994
780
  };
995
781
  }
996
- rows(rows) {
782
+ withRows(rows) {
997
783
  return {
998
784
  dao: this,
999
- rows,
785
+ rows: rows,
1000
786
  };
1001
787
  }
1002
- /**
1003
- * Very @experimental.
1004
- */
1005
- static async multiGetById(inputs, opt = {}) {
1006
- const inputs2 = inputs.map(input => ({
1007
- dao: input.dao,
1008
- ids: [input.id],
1009
- }));
1010
- const rowsByTable = await CommonDao.multiGetByIds(inputs2, opt);
1011
- const results = inputs.map(({ dao }) => {
1012
- const { table } = dao.cfg;
1013
- return rowsByTable[table]?.[0] || null;
1014
- });
1015
- return results;
788
+ withRow(row, opt) {
789
+ return {
790
+ dao: this,
791
+ row: row,
792
+ opt: opt,
793
+ };
1016
794
  }
1017
795
  /**
1018
- * Very @experimental.
796
+ * Load rows (by their ids) from Multiple tables at once.
797
+ * An optimized way to load data, minimizing DB round-trips.
798
+ *
799
+ * @experimental.
1019
800
  */
1020
- static async multiGetByIds(inputs, opt = {}) {
1021
- if (!inputs.length)
801
+ static async multiGet(inputMap, opt = {}) {
802
+ const db = Object.values(inputMap)[0]?.dao.cfg.db;
803
+ if (!db) {
1022
804
  return {};
1023
- const { db } = inputs[0].dao.cfg;
1024
- const idsByTable = {};
1025
- for (const { dao, ids } of inputs) {
1026
- const { table } = dao.cfg;
1027
- idsByTable[table] = ids;
1028
805
  }
806
+ const idsByTable = CommonDao.prepareMultiGetIds(inputMap);
1029
807
  // todo: support tx
1030
- const dbmsByTable = await db.multiGetByIds(idsByTable, opt);
1031
- const bmsByTable = {};
1032
- await pMap(inputs, async ({ dao }) => {
808
+ const dbmsByTable = await db.multiGet(idsByTable, opt);
809
+ const dbmByTableById = CommonDao.multiGetMapByTableById(dbmsByTable);
810
+ return (await CommonDao.prepareMultiGetOutput(inputMap, dbmByTableById, opt));
811
+ }
812
+ static prepareMultiGetIds(inputMap) {
813
+ const idSetByTable = {};
814
+ for (const input of _stringMapValues(inputMap)) {
815
+ const { table } = input.dao.cfg;
816
+ idSetByTable[table] ||= new Set();
817
+ if ('id' in input) {
818
+ // Singular
819
+ idSetByTable[table].add(input.id);
820
+ }
821
+ else {
822
+ // Plural
823
+ for (const id of input.ids) {
824
+ idSetByTable[table].add(id);
825
+ }
826
+ }
827
+ }
828
+ const idsByTable = {};
829
+ for (const [table, idSet] of _stringMapEntries(idSetByTable)) {
830
+ idsByTable[table] = [...idSet];
831
+ }
832
+ return idsByTable;
833
+ }
834
+ static multiGetMapByTableById(dbmsByTable) {
835
+ // We create this "map of maps", to be able to track the results back to the input props
836
+ // This is needed to support:
837
+ // - having multiple props from the same table
838
+ const dbmByTableById = {};
839
+ for (const [table, dbms] of _stringMapEntries(dbmsByTable)) {
840
+ dbmByTableById[table] ||= {};
841
+ for (const dbm of dbms) {
842
+ dbmByTableById[table][dbm.id] = dbm;
843
+ }
844
+ }
845
+ return dbmByTableById;
846
+ }
847
+ static async prepareMultiGetOutput(inputMap, dbmByTableById, opt = {}) {
848
+ const bmsByProp = {};
849
+ // Loop over input props again, to produce the output of the same shape as requested
850
+ await pMap(_stringMapEntries(inputMap), async ([prop, input]) => {
851
+ const { dao } = input;
1033
852
  const { table } = dao.cfg;
1034
- let dbms = dbmsByTable[table] || [];
1035
- if (dao.cfg.hooks.afterLoad && dbms.length) {
1036
- dbms = (await pMap(dbms, async (dbm) => await dao.cfg.hooks.afterLoad(dbm))).filter(_isTruthy);
853
+ if ('id' in input) {
854
+ // Singular
855
+ const dbm = dbmByTableById[table][input.id];
856
+ bmsByProp[prop] = (await dao.dbmToBM(dbm, opt)) || null;
857
+ }
858
+ else {
859
+ // Plural
860
+ // We apply filtering, to be able to support multiple input props fetching from the same table.
861
+ // Without filtering - every prop will get ALL rows from that table.
862
+ const dbms = input.ids.map(id => dbmByTableById[table][id]).filter(_isTruthy);
863
+ bmsByProp[prop] = await dao.dbmsToBM(dbms, opt);
1037
864
  }
1038
- bmsByTable[table] = await dao.dbmsToBM(dbms, opt);
1039
865
  });
1040
- return bmsByTable;
866
+ return bmsByProp;
1041
867
  }
1042
868
  /**
1043
869
  * Very @experimental.
@@ -1050,19 +876,40 @@ export class CommonDao {
1050
876
  for (const { dao, ids } of inputs) {
1051
877
  idsByTable[dao.cfg.table] = ids;
1052
878
  }
1053
- return await db.multiDeleteByIds(idsByTable);
879
+ return await db.multiDelete(idsByTable);
1054
880
  }
1055
- static async multiSaveBatch(inputs, opt = {}) {
881
+ static async multiSave(inputs, opt = {}) {
1056
882
  if (!inputs.length)
1057
883
  return;
1058
884
  const { db } = inputs[0].dao.cfg;
1059
885
  const dbmsByTable = {};
1060
- await pMap(inputs, async ({ dao, rows }) => {
886
+ await pMap(inputs, async (input) => {
887
+ const { dao } = input;
1061
888
  const { table } = dao.cfg;
1062
- rows.forEach(bm => dao.assignIdCreatedUpdated(bm, opt));
1063
- dbmsByTable[table] = await dao.bmsToDBM(rows, opt);
889
+ dbmsByTable[table] ||= [];
890
+ if ('row' in input) {
891
+ // Singular
892
+ const { row } = input;
893
+ if (input.opt?.skipIfEquals) {
894
+ // We compare with convertedBM, to account for cases when some extra property is assigned to bm,
895
+ // which should be removed post-validation, but it breaks the "equality check"
896
+ // Post-validation the equality check should work as intended
897
+ const convertedBM = dao.validateAndConvert(row, 'save', opt);
898
+ if (_deepJsonEquals(convertedBM, input.opt.skipIfEquals)) {
899
+ // Skipping the save operation
900
+ return;
901
+ }
902
+ }
903
+ dao.assignIdCreatedUpdated(row, opt);
904
+ dbmsByTable[table].push(await dao.bmToDBM(row, opt));
905
+ }
906
+ else {
907
+ // Plural
908
+ input.rows.forEach(bm => dao.assignIdCreatedUpdated(bm, opt));
909
+ dbmsByTable[table].push(...(await dao.bmsToDBM(input.rows, opt)));
910
+ }
1064
911
  });
1065
- await db.multiSaveBatch(dbmsByTable);
912
+ await db.multiSave(dbmsByTable);
1066
913
  }
1067
914
  async createTransaction(opt) {
1068
915
  const tx = await this.cfg.db.createTransaction(opt);
@@ -1082,6 +929,30 @@ export class CommonDao {
1082
929
  }, opt);
1083
930
  return r;
1084
931
  }
932
+ ensureRequired(row, id, opt) {
933
+ const table = opt.table || this.cfg.table;
934
+ _assert(row, `DB row required, but not found in ${table}`, {
935
+ table,
936
+ id,
937
+ });
938
+ return row; // pass-through
939
+ }
940
+ /**
941
+ * Throws if readOnly is true
942
+ */
943
+ requireWriteAccess() {
944
+ _assert(!this.cfg.readOnly, DBLibError.DAO_IS_READ_ONLY, {
945
+ table: this.cfg.table,
946
+ });
947
+ }
948
+ /**
949
+ * Throws if readOnly is true
950
+ */
951
+ requireObjectMutability(opt) {
952
+ _assert(!this.cfg.immutable || opt.allowMutability, DBLibError.OBJECT_IS_IMMUTABLE, {
953
+ table: this.cfg.table,
954
+ });
955
+ }
1085
956
  /**
1086
957
  * Throws if query uses a property that is in `excludeFromIndexes` list.
1087
958
  */
@@ -1095,134 +966,4 @@ export class CommonDao {
1095
966
  });
1096
967
  }
1097
968
  }
1098
- logResult(started, op, res, table) {
1099
- if (!this.cfg.logLevel)
1100
- return;
1101
- let logRes;
1102
- const args = [];
1103
- if (Array.isArray(res)) {
1104
- logRes = `${res.length} row(s)`;
1105
- if (res.length && this.cfg.logLevel >= CommonDaoLogLevel.DATA_FULL) {
1106
- args.push('\n', ...res.slice(0, 10)); // max 10 items
1107
- }
1108
- }
1109
- else if (res) {
1110
- logRes = `1 row`;
1111
- if (this.cfg.logLevel >= CommonDaoLogLevel.DATA_SINGLE) {
1112
- args.push('\n', res);
1113
- }
1114
- }
1115
- else {
1116
- logRes = `undefined`;
1117
- }
1118
- this.cfg.logger?.log(`<< ${table}.${op}: ${logRes} in ${_since(started)}`, ...args);
1119
- }
1120
- logSaveResult(started, op, table) {
1121
- if (!this.cfg.logLevel)
1122
- return;
1123
- this.cfg.logger?.log(`<< ${table}.${op} in ${_since(started)}`);
1124
- }
1125
- logStarted(op, table, force = false) {
1126
- if (this.cfg.logStarted || force) {
1127
- this.cfg.logger?.log(`>> ${table}.${op}`);
1128
- }
1129
- return localTime.nowUnixMillis();
1130
- }
1131
- logSaveStarted(op, items, table) {
1132
- if (this.cfg.logStarted) {
1133
- const args = [`>> ${table}.${op}`];
1134
- if (Array.isArray(items)) {
1135
- if (items.length && this.cfg.logLevel >= CommonDaoLogLevel.DATA_FULL) {
1136
- args.push('\n', ...items.slice(0, 10));
1137
- }
1138
- else {
1139
- args.push(`${items.length} row(s)`);
1140
- }
1141
- }
1142
- else {
1143
- if (this.cfg.logLevel >= CommonDaoLogLevel.DATA_SINGLE) {
1144
- args.push(items);
1145
- }
1146
- }
1147
- this.cfg.logger?.log(...args);
1148
- }
1149
- return localTime.nowUnixMillis();
1150
- }
1151
- }
1152
- /**
1153
- * Transaction context.
1154
- * Has similar API than CommonDao, but all operations are performed in the context of the transaction.
1155
- */
1156
- export class CommonDaoTransaction {
1157
- tx;
1158
- logger;
1159
- constructor(tx, logger) {
1160
- this.tx = tx;
1161
- this.logger = logger;
1162
- }
1163
- /**
1164
- * Commits the underlying DBTransaction.
1165
- * May throw.
1166
- */
1167
- async commit() {
1168
- await this.tx.commit();
1169
- }
1170
- /**
1171
- * Perform a graceful rollback without throwing/re-throwing any error.
1172
- * Never throws.
1173
- */
1174
- async rollback() {
1175
- try {
1176
- await this.tx.rollback();
1177
- }
1178
- catch (err) {
1179
- // graceful rollback without re-throw
1180
- this.logger.error(err);
1181
- }
1182
- }
1183
- async getById(dao, id, opt) {
1184
- return await dao.getById(id, { ...opt, tx: this.tx });
1185
- }
1186
- async getByIds(dao, ids, opt) {
1187
- return await dao.getByIds(ids, { ...opt, tx: this.tx });
1188
- }
1189
- // todo: Queries inside Transaction are not supported yet
1190
- // async runQuery<BM extends PartialObjectWithId, DBM extends ObjectWithId>(
1191
- // dao: CommonDao<BM, DBM, any>,
1192
- // q: DBQuery<DBM>,
1193
- // opt?: CommonDaoOptions,
1194
- // ): Promise<BM[]> {
1195
- // try {
1196
- // return await dao.runQuery(q, { ...opt, tx: this.tx })
1197
- // } catch (err) {
1198
- // await this.rollback()
1199
- // throw err
1200
- // }
1201
- // }
1202
- async save(dao, bm, opt) {
1203
- return await dao.save(bm, { ...opt, tx: this.tx });
1204
- }
1205
- async saveBatch(dao, bms, opt) {
1206
- return await dao.saveBatch(bms, { ...opt, tx: this.tx });
1207
- }
1208
- /**
1209
- * DaoTransaction.patch does not load from DB.
1210
- * It assumes the bm was previously loaded in the same Transaction, hence could not be
1211
- * concurrently modified. Hence it's safe to not sync with DB.
1212
- *
1213
- * So, this method is a rather simple convenience "Object.assign and then save".
1214
- */
1215
- async patch(dao, bm, patch, opt) {
1216
- const skipIfEquals = _deepCopy(bm);
1217
- Object.assign(bm, patch);
1218
- return await dao.save(bm, { ...opt, skipIfEquals, tx: this.tx });
1219
- }
1220
- async deleteById(dao, id, opt) {
1221
- if (!id)
1222
- return 0;
1223
- return await this.deleteByIds(dao, [id], opt);
1224
- }
1225
- async deleteByIds(dao, ids, opt) {
1226
- return await dao.deleteByIds(ids, { ...opt, tx: this.tx });
1227
- }
1228
969
  }