@naturalcycles/db-lib 10.19.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,20 +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';
10
8
  import { _stringMapEntries, _stringMapValues, } from '@naturalcycles/js-lib/types';
11
9
  import { _passthroughPredicate, _typeCast, SKIP } from '@naturalcycles/js-lib/types';
12
10
  import { stringId } from '@naturalcycles/nodejs-lib';
13
- import { transformFlatten } from '@naturalcycles/nodejs-lib/stream';
11
+ import { transformFlatten, transformMapSync, } from '@naturalcycles/nodejs-lib/stream';
14
12
  import { _pipeline, transformChunk, transformLogProgress, transformMap, transformNoOp, writableVoid, } from '@naturalcycles/nodejs-lib/stream';
15
13
  import { DBLibError } from '../cnst.js';
16
14
  import { RunnableDBQuery } from '../query/dbQuery.js';
17
- import { CommonDaoLogLevel } from './common.dao.model.js';
15
+ import { CommonDaoTransaction } from './commonDaoTransaction.js';
18
16
  /**
19
17
  * Lowest common denominator API between supported Databases.
20
18
  *
@@ -27,7 +25,6 @@ export class CommonDao {
27
25
  constructor(cfg) {
28
26
  this.cfg = cfg;
29
27
  this.cfg = {
30
- logLevel: CommonDaoLogLevel.NONE,
31
28
  generateId: true,
32
29
  assignGeneratedIds: false,
33
30
  useCreatedProperty: true,
@@ -59,22 +56,13 @@ export class CommonDao {
59
56
  return this.validateAndConvert(bm, undefined, opt);
60
57
  }
61
58
  // GET
62
- // overrides are disabled now, as they obfuscate errors when ID branded type is used
63
- // async getById(id: undefined | null, opt?: CommonDaoOptions): Promise<null>
64
- // async getById(id?: ID | null, opt?: CommonDaoOptions): Promise<BM | null>
65
- async getById(id, opt = {}) {
66
- if (!id)
67
- return null;
68
- const op = `getById(${id})`;
69
- const table = opt.table || this.cfg.table;
70
- const started = this.logStarted(op, table);
71
- let dbm = (await (opt.tx || this.cfg.db).getByIds(table, [id], opt))[0];
72
- if (dbm && this.cfg.hooks.afterLoad) {
73
- dbm = (await this.cfg.hooks.afterLoad(dbm)) || undefined;
74
- }
75
- const bm = await this.dbmToBM(dbm, opt);
76
- this.logResult(started, op, bm, table);
77
- 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);
78
66
  }
79
67
  async getByIdOrEmpty(id, part = {}, opt) {
80
68
  const bm = await this.getById(id, opt);
@@ -82,99 +70,32 @@ export class CommonDao {
82
70
  return bm;
83
71
  return this.create({ ...part, id }, opt);
84
72
  }
85
- // async getByIdAsDBM(id: undefined | null, opt?: CommonDaoOptions): Promise<null>
86
- // 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
+ }
87
79
  async getByIdAsDBM(id, opt = {}) {
88
80
  if (!id)
89
81
  return null;
90
- const op = `getByIdAsDBM(${id})`;
91
- const table = opt.table || this.cfg.table;
92
- const started = this.logStarted(op, table);
93
- let [dbm] = await (opt.tx || this.cfg.db).getByIds(table, [id], opt);
94
- if (dbm && this.cfg.hooks.afterLoad) {
95
- dbm = (await this.cfg.hooks.afterLoad(dbm)) || undefined;
96
- }
97
- dbm = this.anyToDBM(dbm, opt);
98
- this.logResult(started, op, dbm, table);
99
- return dbm || null;
82
+ const [row] = await this.loadByIds([id], opt);
83
+ return this.anyToDBM(row, opt) || null;
100
84
  }
101
85
  async getByIds(ids, opt = {}) {
102
- if (!ids.length)
103
- return [];
104
- const op = `getByIds ${ids.length} id(s) (${_truncate(ids.slice(0, 10).join(', '), 50)})`;
105
- const table = opt.table || this.cfg.table;
106
- const started = this.logStarted(op, table);
107
- let dbms = await (opt.tx || this.cfg.db).getByIds(table, ids, opt);
108
- if (this.cfg.hooks.afterLoad && dbms.length) {
109
- dbms = (await pMap(dbms, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(_isTruthy);
110
- }
111
- const bms = await this.dbmsToBM(dbms, opt);
112
- this.logResult(started, op, bms, table);
113
- return bms;
86
+ const dbms = await this.loadByIds(ids, opt);
87
+ return await this.dbmsToBM(dbms, opt);
114
88
  }
115
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 = {}) {
116
95
  if (!ids.length)
117
96
  return [];
118
- const op = `getByIdsAsDBM ${ids.length} id(s) (${_truncate(ids.slice(0, 10).join(', '), 50)})`;
119
- const table = opt.table || this.cfg.table;
120
- const started = this.logStarted(op, table);
121
- let dbms = await (opt.tx || this.cfg.db).getByIds(table, ids, opt);
122
- if (this.cfg.hooks.afterLoad && dbms.length) {
123
- dbms = (await pMap(dbms, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(_isTruthy);
124
- }
125
- this.logResult(started, op, dbms, table);
126
- return dbms;
127
- }
128
- async requireById(id, opt = {}) {
129
- const r = await this.getById(id, opt);
130
- if (!r) {
131
- this.throwRequiredError(id, opt);
132
- }
133
- return r;
134
- }
135
- async requireByIdAsDBM(id, opt = {}) {
136
- const r = await this.getByIdAsDBM(id, opt);
137
- if (!r) {
138
- this.throwRequiredError(id, opt);
139
- }
140
- return r;
141
- }
142
- throwRequiredError(id, opt) {
143
97
  const table = opt.table || this.cfg.table;
144
- throw new AppError(`DB row required, but not found in ${table}`, {
145
- table,
146
- id,
147
- });
148
- }
149
- /**
150
- * Throws if readOnly is true
151
- */
152
- requireWriteAccess() {
153
- if (this.cfg.readOnly) {
154
- throw new AppError(DBLibError.DAO_IS_READ_ONLY, {
155
- table: this.cfg.table,
156
- });
157
- }
158
- }
159
- /**
160
- * Throws if readOnly is true
161
- */
162
- requireObjectMutability(opt) {
163
- if (this.cfg.immutable && !opt.allowMutability) {
164
- throw new AppError(DBLibError.OBJECT_IS_IMMUTABLE, {
165
- table: this.cfg.table,
166
- });
167
- }
168
- }
169
- async ensureUniqueId(table, dbm) {
170
- // todo: retry N times
171
- const existing = await this.cfg.db.getByIds(table, [dbm.id]);
172
- if (existing.length) {
173
- throw new AppError(DBLibError.NON_UNIQUE_ID, {
174
- table,
175
- ids: existing.map(i => i.id),
176
- });
177
- }
98
+ return await (opt.tx || this.cfg.db).getByIds(table, ids, opt);
178
99
  }
179
100
  async getBy(by, value, limit = 0, opt) {
180
101
  return await this.query().filterEq(by, value).limit(limit).runQuery(opt);
@@ -215,15 +136,9 @@ export class CommonDao {
215
136
  async runQueryExtended(q, opt = {}) {
216
137
  this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
217
138
  q.table = opt.table || q.table;
218
- const op = `runQuery(${q.pretty()})`;
219
- const started = this.logStarted(op, q.table);
220
- let { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
221
- const partialQuery = !!q._selectedFieldNames;
222
- if (this.cfg.hooks.afterLoad && rows.length) {
223
- rows = (await pMap(rows, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(_isTruthy);
224
- }
225
- const bms = partialQuery ? rows : await this.dbmsToBM(rows, opt);
226
- 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);
227
142
  return {
228
143
  rows: bms,
229
144
  ...queryResult,
@@ -236,48 +151,27 @@ export class CommonDao {
236
151
  async runQueryExtendedAsDBM(q, opt = {}) {
237
152
  this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
238
153
  q.table = opt.table || q.table;
239
- const op = `runQueryAsDBM(${q.pretty()})`;
240
- const started = this.logStarted(op, q.table);
241
- let { rows, ...queryResult } = await this.cfg.db.runQuery(q, opt);
242
- if (this.cfg.hooks.afterLoad && rows.length) {
243
- rows = (await pMap(rows, async (dbm) => await this.cfg.hooks.afterLoad(dbm))).filter(_isTruthy);
244
- }
245
- const partialQuery = !!q._selectedFieldNames;
246
- const dbms = partialQuery ? rows : this.anyToDBMs(rows, opt);
247
- 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);
248
157
  return { rows: dbms, ...queryResult };
249
158
  }
250
159
  async runQueryCount(q, opt = {}) {
251
160
  this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
252
161
  q.table = opt.table || q.table;
253
- const op = `runQueryCount(${q.pretty()})`;
254
- const started = this.logStarted(op, q.table);
255
- const count = await this.cfg.db.runQueryCount(q, opt);
256
- if (this.cfg.logLevel >= CommonDaoLogLevel.OPERATIONS) {
257
- this.cfg.logger?.log(`<< ${q.table}.${op}: ${count} row(s) in ${_since(started)}`);
258
- }
259
- return count;
162
+ return await this.cfg.db.runQueryCount(q, opt);
260
163
  }
261
164
  async streamQueryForEach(q, mapper, opt = {}) {
262
165
  this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
263
166
  q.table = opt.table || q.table;
264
167
  opt.skipValidation = opt.skipValidation !== false; // default true
265
168
  opt.errorMode ||= ErrorMode.SUPPRESS;
266
- const partialQuery = !!q._selectedFieldNames;
267
- const op = `streamQueryForEach(${q.pretty()})`;
268
- const started = this.logStarted(op, q.table, true);
269
- let count = 0;
169
+ const isPartialQuery = !!q._selectedFieldNames;
270
170
  await _pipeline([
271
171
  this.cfg.db.streamQuery(q, opt),
272
172
  transformMap(async (dbm) => {
273
- count++;
274
- if (partialQuery)
173
+ if (isPartialQuery)
275
174
  return dbm;
276
- if (this.cfg.hooks.afterLoad) {
277
- dbm = (await this.cfg.hooks.afterLoad(dbm));
278
- if (dbm === null)
279
- return SKIP;
280
- }
281
175
  return await this.dbmToBM(dbm, opt);
282
176
  }, {
283
177
  errorMode: opt.errorMode,
@@ -293,30 +187,18 @@ export class CommonDao {
293
187
  }),
294
188
  writableVoid(),
295
189
  ]);
296
- if (this.cfg.logLevel >= CommonDaoLogLevel.OPERATIONS) {
297
- this.cfg.logger?.log(`<< ${q.table}.${op}: ${count} row(s) in ${_since(started)}`);
298
- }
299
190
  }
300
191
  async streamQueryAsDBMForEach(q, mapper, opt = {}) {
301
192
  this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
302
193
  q.table = opt.table || q.table;
303
194
  opt.skipValidation = opt.skipValidation !== false; // default true
304
195
  opt.errorMode ||= ErrorMode.SUPPRESS;
305
- const partialQuery = !!q._selectedFieldNames;
306
- const op = `streamQueryAsDBMForEach(${q.pretty()})`;
307
- const started = this.logStarted(op, q.table, true);
308
- let count = 0;
196
+ const isPartialQuery = !!q._selectedFieldNames;
309
197
  await _pipeline([
310
198
  this.cfg.db.streamQuery(q, opt),
311
- transformMap(async (dbm) => {
312
- count++;
313
- if (partialQuery)
199
+ transformMapSync(dbm => {
200
+ if (isPartialQuery)
314
201
  return dbm;
315
- if (this.cfg.hooks.afterLoad) {
316
- dbm = (await this.cfg.hooks.afterLoad(dbm));
317
- if (dbm === null)
318
- return SKIP;
319
- }
320
202
  return this.anyToDBM(dbm, opt);
321
203
  }, {
322
204
  errorMode: opt.errorMode,
@@ -332,9 +214,6 @@ export class CommonDao {
332
214
  }),
333
215
  writableVoid(),
334
216
  ]);
335
- if (this.cfg.logLevel >= CommonDaoLogLevel.OPERATIONS) {
336
- this.cfg.logger?.log(`<< ${q.table}.${op}: ${count} row(s) in ${_since(started)}`);
337
- }
338
217
  }
339
218
  /**
340
219
  * Stream as Readable, to be able to .pipe() it further with support of backpressure.
@@ -344,19 +223,14 @@ export class CommonDao {
344
223
  q.table = opt.table || q.table;
345
224
  opt.skipValidation = opt.skipValidation !== false; // default true
346
225
  opt.errorMode ||= ErrorMode.SUPPRESS;
347
- const partialQuery = !!q._selectedFieldNames;
226
+ const isPartialQuery = !!q._selectedFieldNames;
348
227
  const stream = this.cfg.db.streamQuery(q, opt);
349
- if (partialQuery)
228
+ if (isPartialQuery)
350
229
  return stream;
351
230
  return (stream
352
231
  // the commented out line was causing RangeError: Maximum call stack size exceeded
353
232
  // .on('error', err => stream.emit('error', err))
354
- .pipe(transformMap(async (dbm) => {
355
- if (this.cfg.hooks.afterLoad) {
356
- dbm = (await this.cfg.hooks.afterLoad(dbm));
357
- if (dbm === null)
358
- return SKIP;
359
- }
233
+ .pipe(transformMapSync(dbm => {
360
234
  return this.anyToDBM(dbm, opt);
361
235
  }, {
362
236
  errorMode: opt.errorMode,
@@ -377,16 +251,11 @@ export class CommonDao {
377
251
  opt.skipValidation = opt.skipValidation !== false; // default true
378
252
  opt.errorMode ||= ErrorMode.SUPPRESS;
379
253
  const stream = this.cfg.db.streamQuery(q, opt);
380
- const partialQuery = !!q._selectedFieldNames;
381
- if (partialQuery)
254
+ const isPartialQuery = !!q._selectedFieldNames;
255
+ if (isPartialQuery)
382
256
  return stream;
383
257
  // This almost works, but hard to implement `errorMode: THROW_AGGREGATED` in this case
384
258
  // return stream.flatMap(async (dbm: DBM) => {
385
- // if (this.cfg.hooks!.afterLoad) {
386
- // dbm = (await this.cfg.hooks!.afterLoad(dbm))!
387
- // if (dbm === null) return [] // SKIP
388
- // }
389
- //
390
259
  // return [await this.dbmToBM(dbm, opt)] satisfies BM[]
391
260
  // }, {
392
261
  // concurrency: 16,
@@ -398,11 +267,6 @@ export class CommonDao {
398
267
  // the commented out line was causing RangeError: Maximum call stack size exceeded
399
268
  // .on('error', err => stream.emit('error', err))
400
269
  .pipe(transformMap(async (dbm) => {
401
- if (this.cfg.hooks.afterLoad) {
402
- dbm = (await this.cfg.hooks.afterLoad(dbm));
403
- if (dbm === null)
404
- return SKIP;
405
- }
406
270
  return await this.dbmToBM(dbm, opt);
407
271
  }, {
408
272
  errorMode: opt.errorMode,
@@ -442,14 +306,8 @@ export class CommonDao {
442
306
  this.validateQueryIndexes(q); // throws if query uses `excludeFromIndexes` property
443
307
  q.table = opt.table || q.table;
444
308
  opt.errorMode ||= ErrorMode.SUPPRESS;
445
- const op = `streamQueryIdsForEach(${q.pretty()})`;
446
- const started = this.logStarted(op, q.table, true);
447
- let count = 0;
448
309
  await _pipeline([
449
- this.cfg.db.streamQuery(q.select(['id']), opt).map(r => {
450
- count++;
451
- return r.id;
452
- }),
310
+ this.cfg.db.streamQuery(q.select(['id']), opt).map(r => r.id),
453
311
  transformMap(mapper, {
454
312
  ...opt,
455
313
  predicate: _passthroughPredicate,
@@ -461,13 +319,9 @@ export class CommonDao {
461
319
  }),
462
320
  writableVoid(),
463
321
  ]);
464
- if (this.cfg.logLevel >= CommonDaoLogLevel.OPERATIONS) {
465
- this.cfg.logger?.log(`<< ${q.table}.${op}: ${count} id(s) in ${_since(started)}`);
466
- }
467
322
  }
468
323
  /**
469
324
  * Mutates!
470
- * "Returns", just to have a type of "Saved"
471
325
  */
472
326
  assignIdCreatedUpdated(obj, opt = {}) {
473
327
  const now = localTime.nowUnix();
@@ -481,7 +335,6 @@ export class CommonDao {
481
335
  obj.id ||= (this.cfg.hooks.createNaturalId?.(obj) ||
482
336
  this.cfg.hooks.createRandomId());
483
337
  }
484
- return obj;
485
338
  }
486
339
  // SAVE
487
340
  /**
@@ -599,7 +452,6 @@ export class CommonDao {
599
452
  return bm;
600
453
  }
601
454
  }
602
- const idWasGenerated = !bm.id && this.cfg.generateId;
603
455
  this.assignIdCreatedUpdated(bm, opt); // mutates
604
456
  _typeCast(bm);
605
457
  let dbm = await this.bmToDBM(bm, opt); // validates BM
@@ -609,125 +461,76 @@ export class CommonDao {
609
461
  return bm;
610
462
  }
611
463
  const table = opt.table || this.cfg.table;
612
- if (opt.ensureUniqueId && idWasGenerated)
613
- await this.ensureUniqueId(table, dbm);
614
- if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
615
- opt = { ...opt, saveMethod: 'insert' };
616
- }
617
- const op = `save(${dbm.id})`;
618
- const started = this.logSaveStarted(op, bm, table);
619
- const { excludeFromIndexes } = this.cfg;
620
- const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds;
621
- await (opt.tx || this.cfg.db).saveBatch(table, [dbm], {
622
- excludeFromIndexes,
623
- assignGeneratedIds,
624
- ...opt,
625
- });
626
- if (assignGeneratedIds) {
464
+ const saveOptions = this.prepareSaveOptions(opt);
465
+ await (opt.tx || this.cfg.db).saveBatch(table, [dbm], saveOptions);
466
+ if (saveOptions.assignGeneratedIds) {
627
467
  bm.id = dbm.id;
628
468
  }
629
- this.logSaveResult(started, op, table);
630
469
  return bm;
631
470
  }
632
471
  async saveAsDBM(dbm, opt = {}) {
633
472
  this.requireWriteAccess();
634
- const table = opt.table || this.cfg.table;
635
- // assigning id in case it misses the id
636
- // will override/set `updated` field, unless opts.preserveUpdated is set
637
- const idWasGenerated = !dbm.id && this.cfg.generateId;
638
473
  this.assignIdCreatedUpdated(dbm, opt); // mutates
639
474
  let row = this.anyToDBM(dbm, opt);
640
- if (opt.ensureUniqueId && idWasGenerated)
641
- await this.ensureUniqueId(table, row);
642
- if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
643
- opt = { ...opt, saveMethod: 'insert' };
644
- }
645
- const op = `saveAsDBM(${row.id})`;
646
- const started = this.logSaveStarted(op, row, table);
647
- const { excludeFromIndexes } = this.cfg;
648
- const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds;
649
475
  if (this.cfg.hooks.beforeSave) {
650
476
  row = (await this.cfg.hooks.beforeSave(row));
651
477
  if (row === null)
652
478
  return dbm;
653
479
  }
654
- await (opt.tx || this.cfg.db).saveBatch(table, [row], {
655
- excludeFromIndexes,
656
- assignGeneratedIds,
657
- ...opt,
658
- });
659
- 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) {
660
484
  dbm.id = row.id;
661
485
  }
662
- this.logSaveResult(started, op, table);
663
486
  return row;
664
487
  }
665
488
  async saveBatch(bms, opt = {}) {
666
489
  if (!bms.length)
667
490
  return [];
668
491
  this.requireWriteAccess();
669
- const table = opt.table || this.cfg.table;
670
492
  bms.forEach(bm => this.assignIdCreatedUpdated(bm, opt));
671
493
  let dbms = await this.bmsToDBM(bms, opt);
672
494
  if (this.cfg.hooks.beforeSave && dbms.length) {
673
495
  dbms = (await pMap(dbms, async (dbm) => await this.cfg.hooks.beforeSave(dbm))).filter(_isTruthy);
674
496
  }
675
- if (opt.ensureUniqueId)
676
- throw new AppError('ensureUniqueId is not supported in saveBatch');
677
- if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
678
- opt = { ...opt, saveMethod: 'insert' };
679
- }
680
- const op = `saveBatch ${dbms.length} row(s) (${_truncate(dbms
681
- .slice(0, 10)
682
- .map(bm => bm.id)
683
- .join(', '), 50)})`;
684
- const started = this.logSaveStarted(op, bms, table);
685
- const { excludeFromIndexes } = this.cfg;
686
- const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds;
687
- await (opt.tx || this.cfg.db).saveBatch(table, dbms, {
688
- excludeFromIndexes,
689
- assignGeneratedIds,
690
- ...opt,
691
- });
692
- 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) {
693
501
  dbms.forEach((dbm, i) => (bms[i].id = dbm.id));
694
502
  }
695
- this.logSaveResult(started, op, table);
696
503
  return bms;
697
504
  }
698
505
  async saveBatchAsDBM(dbms, opt = {}) {
699
506
  if (!dbms.length)
700
507
  return [];
701
508
  this.requireWriteAccess();
702
- const table = opt.table || this.cfg.table;
703
- dbms.forEach(dbm => this.assignIdCreatedUpdated(dbm, opt)); // mutates
509
+ dbms.forEach(dbm => this.assignIdCreatedUpdated(dbm, opt));
704
510
  let rows = this.anyToDBMs(dbms, opt);
705
- if (opt.ensureUniqueId)
706
- throw new AppError('ensureUniqueId is not supported in saveBatch');
707
- if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
708
- opt = { ...opt, saveMethod: 'insert' };
709
- }
710
- const op = `saveBatchAsDBM ${rows.length} row(s) (${_truncate(rows
711
- .slice(0, 10)
712
- .map(bm => bm.id)
713
- .join(', '), 50)})`;
714
- const started = this.logSaveStarted(op, rows, table);
715
- const { excludeFromIndexes } = this.cfg;
716
- const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds;
717
511
  if (this.cfg.hooks.beforeSave && rows.length) {
718
512
  rows = (await pMap(rows, async (row) => await this.cfg.hooks.beforeSave(row))).filter(_isTruthy);
719
513
  }
720
- await (opt.tx || this.cfg.db).saveBatch(table, rows, {
721
- excludeFromIndexes,
722
- assignGeneratedIds,
723
- ...opt,
724
- });
725
- 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) {
726
518
  rows.forEach((row, i) => (dbms[i].id = row.id));
727
519
  }
728
- this.logSaveResult(started, op, table);
729
520
  return rows;
730
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
+ }
731
534
  /**
732
535
  * "Streaming" is implemented by buffering incoming rows into **batches**
733
536
  * (of size opt.chunkSize, which defaults to 500),
@@ -793,12 +596,8 @@ export class CommonDao {
793
596
  return 0;
794
597
  this.requireWriteAccess();
795
598
  this.requireObjectMutability(opt);
796
- const op = `deleteByIds(${ids.join(', ')})`;
797
599
  const table = opt.table || this.cfg.table;
798
- const started = this.logStarted(op, table);
799
- const count = await (opt.tx || this.cfg.db).deleteByIds(table, ids, opt);
800
- this.logSaveResult(started, op, table);
801
- return count;
600
+ return await (opt.tx || this.cfg.db).deleteByIds(table, ids, opt);
802
601
  }
803
602
  /**
804
603
  * Pass `chunkSize: number` (e.g 500) option to use Streaming: it will Stream the query, chunk by 500, and execute
@@ -810,8 +609,6 @@ export class CommonDao {
810
609
  this.requireWriteAccess();
811
610
  this.requireObjectMutability(opt);
812
611
  q.table = opt.table || q.table;
813
- const op = `deleteByQuery(${q.pretty()})`;
814
- const started = this.logStarted(op, q.table);
815
612
  let deleted = 0;
816
613
  if (opt.chunkSize) {
817
614
  const { chunkSize, chunkConcurrency = 8 } = opt;
@@ -839,7 +636,6 @@ export class CommonDao {
839
636
  else {
840
637
  deleted = await this.cfg.db.deleteByQuery(q, opt);
841
638
  }
842
- this.logSaveResult(started, op, q.table);
843
639
  return deleted;
844
640
  }
845
641
  async patchByIds(ids, patch, opt = {}) {
@@ -852,11 +648,7 @@ export class CommonDao {
852
648
  this.requireWriteAccess();
853
649
  this.requireObjectMutability(opt);
854
650
  q.table = opt.table || q.table;
855
- const op = `patchByQuery(${q.pretty()})`;
856
- const started = this.logStarted(op, q.table);
857
- const updated = await this.cfg.db.patchByQuery(q, patch, opt);
858
- this.logSaveResult(started, op, q.table);
859
- return updated;
651
+ return await this.cfg.db.patchByQuery(q, patch, opt);
860
652
  }
861
653
  /**
862
654
  * Caveat: it doesn't update created/updated props.
@@ -867,12 +659,9 @@ export class CommonDao {
867
659
  this.requireWriteAccess();
868
660
  this.requireObjectMutability(opt);
869
661
  const { table } = this.cfg;
870
- const op = `increment`;
871
- const started = this.logStarted(op, table);
872
662
  const result = await this.cfg.db.incrementBatch(table, prop, {
873
663
  [id]: by,
874
664
  });
875
- this.logSaveResult(started, op, table);
876
665
  return result[id];
877
666
  }
878
667
  /**
@@ -884,15 +673,11 @@ export class CommonDao {
884
673
  this.requireWriteAccess();
885
674
  this.requireObjectMutability(opt);
886
675
  const { table } = this.cfg;
887
- const op = `incrementBatch`;
888
- const started = this.logStarted(op, table);
889
- const result = await this.cfg.db.incrementBatch(table, prop, incrementMap);
890
- this.logSaveResult(started, op, table);
891
- return result;
676
+ return await this.cfg.db.incrementBatch(table, prop, incrementMap);
892
677
  }
893
678
  async dbmToBM(_dbm, opt = {}) {
894
679
  if (!_dbm)
895
- return;
680
+ return null;
896
681
  // optimization: no need to run full joi DBM validation, cause BM validation will be run
897
682
  // const dbm = this.anyToDBM(_dbm, opt)
898
683
  let dbm = { ..._dbm, ...this.cfg.hooks.parseNaturalId(_dbm.id) };
@@ -909,7 +694,7 @@ export class CommonDao {
909
694
  }
910
695
  async bmToDBM(bm, opt) {
911
696
  if (bm === undefined)
912
- return;
697
+ return null;
913
698
  // bm gets assigned to the new reference
914
699
  bm = this.validateAndConvert(bm, 'save', opt);
915
700
  // BM > DBM
@@ -921,7 +706,7 @@ export class CommonDao {
921
706
  }
922
707
  anyToDBM(dbm, opt = {}) {
923
708
  if (!dbm)
924
- return;
709
+ return null;
925
710
  // this shouldn't be happening on load! but should on save!
926
711
  // this.assignIdCreatedUpdated(dbm, opt)
927
712
  dbm = { ...dbm, ...this.cfg.hooks.parseNaturalId(dbm.id) };
@@ -934,8 +719,8 @@ export class CommonDao {
934
719
  // return this.validateAndConvert(dbm, this.cfg.dbmSchema, DBModelType.DBM, opt)
935
720
  return dbm;
936
721
  }
937
- anyToDBMs(entities, opt = {}) {
938
- return entities.map(entity => this.anyToDBM(entity, opt));
722
+ anyToDBMs(rows, opt = {}) {
723
+ return rows.map(entity => this.anyToDBM(entity, opt));
939
724
  }
940
725
  /**
941
726
  * Returns *converted value* (NOT the same reference).
@@ -994,10 +779,17 @@ export class CommonDao {
994
779
  ids,
995
780
  };
996
781
  }
997
- toSave(input) {
782
+ withRows(rows) {
783
+ return {
784
+ dao: this,
785
+ rows: rows,
786
+ };
787
+ }
788
+ withRow(row, opt) {
998
789
  return {
999
790
  dao: this,
1000
- rows: [input].flat(),
791
+ row: row,
792
+ opt: opt,
1001
793
  };
1002
794
  }
1003
795
  /**
@@ -1091,10 +883,31 @@ export class CommonDao {
1091
883
  return;
1092
884
  const { db } = inputs[0].dao.cfg;
1093
885
  const dbmsByTable = {};
1094
- await pMap(inputs, async ({ dao, rows }) => {
886
+ await pMap(inputs, async (input) => {
887
+ const { dao } = input;
1095
888
  const { table } = dao.cfg;
1096
- rows.forEach(bm => dao.assignIdCreatedUpdated(bm, opt));
1097
- 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
+ }
1098
911
  });
1099
912
  await db.multiSave(dbmsByTable);
1100
913
  }
@@ -1116,6 +929,30 @@ export class CommonDao {
1116
929
  }, opt);
1117
930
  return r;
1118
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
+ }
1119
956
  /**
1120
957
  * Throws if query uses a property that is in `excludeFromIndexes` list.
1121
958
  */
@@ -1129,135 +966,4 @@ export class CommonDao {
1129
966
  });
1130
967
  }
1131
968
  }
1132
- logResult(started, op, res, table) {
1133
- if (!this.cfg.logLevel)
1134
- return;
1135
- let logRes;
1136
- const args = [];
1137
- if (Array.isArray(res)) {
1138
- logRes = `${res.length} row(s)`;
1139
- if (res.length && this.cfg.logLevel >= CommonDaoLogLevel.DATA_FULL) {
1140
- args.push('\n', ...res.slice(0, 10)); // max 10 items
1141
- }
1142
- }
1143
- else if (res) {
1144
- logRes = `1 row`;
1145
- if (this.cfg.logLevel >= CommonDaoLogLevel.DATA_SINGLE) {
1146
- args.push('\n', res);
1147
- }
1148
- }
1149
- else {
1150
- logRes = `undefined`;
1151
- }
1152
- this.cfg.logger?.log(`<< ${table}.${op}: ${logRes} in ${_since(started)}`, ...args);
1153
- }
1154
- logSaveResult(started, op, table) {
1155
- if (!this.cfg.logLevel)
1156
- return;
1157
- this.cfg.logger?.log(`<< ${table}.${op} in ${_since(started)}`);
1158
- }
1159
- logStarted(op, table, force = false) {
1160
- if (this.cfg.logStarted || force) {
1161
- this.cfg.logger?.log(`>> ${table}.${op}`);
1162
- }
1163
- return localTime.nowUnixMillis();
1164
- }
1165
- logSaveStarted(op, items, table) {
1166
- if (this.cfg.logStarted) {
1167
- const args = [`>> ${table}.${op}`];
1168
- if (Array.isArray(items)) {
1169
- if (items.length && this.cfg.logLevel >= CommonDaoLogLevel.DATA_FULL) {
1170
- args.push('\n', ...items.slice(0, 10));
1171
- }
1172
- else {
1173
- args.push(`${items.length} row(s)`);
1174
- }
1175
- }
1176
- else {
1177
- if (this.cfg.logLevel >= CommonDaoLogLevel.DATA_SINGLE) {
1178
- args.push(items);
1179
- }
1180
- }
1181
- this.cfg.logger?.log(...args);
1182
- }
1183
- return localTime.nowUnixMillis();
1184
- }
1185
- }
1186
- /**
1187
- * Transaction context.
1188
- * Has similar API than CommonDao, but all operations are performed in the context of the transaction.
1189
- */
1190
- export class CommonDaoTransaction {
1191
- tx;
1192
- logger;
1193
- constructor(tx, logger) {
1194
- this.tx = tx;
1195
- this.logger = logger;
1196
- }
1197
- /**
1198
- * Commits the underlying DBTransaction.
1199
- * May throw.
1200
- */
1201
- async commit() {
1202
- await this.tx.commit();
1203
- }
1204
- /**
1205
- * Perform a graceful rollback without throwing/re-throwing any error.
1206
- * Never throws.
1207
- */
1208
- async rollback() {
1209
- try {
1210
- await this.tx.rollback();
1211
- }
1212
- catch (err) {
1213
- // graceful rollback without re-throw
1214
- this.logger.error(err);
1215
- }
1216
- }
1217
- async getById(dao, id, opt) {
1218
- return await dao.getById(id, { ...opt, tx: this.tx });
1219
- }
1220
- async getByIds(dao, ids, opt) {
1221
- return await dao.getByIds(ids, { ...opt, tx: this.tx });
1222
- }
1223
- // todo: Queries inside Transaction are not supported yet
1224
- // async runQuery<BM extends PartialObjectWithId, DBM extends ObjectWithId>(
1225
- // dao: CommonDao<BM, DBM, any>,
1226
- // q: DBQuery<DBM>,
1227
- // opt?: CommonDaoOptions,
1228
- // ): Promise<BM[]> {
1229
- // try {
1230
- // return await dao.runQuery(q, { ...opt, tx: this.tx })
1231
- // } catch (err) {
1232
- // await this.rollback()
1233
- // throw err
1234
- // }
1235
- // }
1236
- async save(dao, bm, opt) {
1237
- return await dao.save(bm, { ...opt, tx: this.tx });
1238
- }
1239
- async saveBatch(dao, bms, opt) {
1240
- return await dao.saveBatch(bms, { ...opt, tx: this.tx });
1241
- }
1242
- /**
1243
- * DaoTransaction.patch does not load from DB.
1244
- * It assumes the bm was previously loaded in the same Transaction, hence could not be
1245
- * concurrently modified. Hence it's safe to not sync with DB.
1246
- *
1247
- * So, this method is a rather simple convenience "Object.assign and then save".
1248
- */
1249
- async patch(dao, bm, patch, opt) {
1250
- const skipIfEquals = _deepCopy(bm);
1251
- Object.assign(bm, patch);
1252
- return await dao.save(bm, { ...opt, skipIfEquals, tx: this.tx });
1253
- }
1254
- // todo: use AnyDao/Infer in other methods as well, if this works well
1255
- async deleteById(dao, id, opt) {
1256
- if (!id)
1257
- return 0;
1258
- return await this.deleteByIds(dao, [id], opt);
1259
- }
1260
- async deleteByIds(dao, ids, opt) {
1261
- return await dao.deleteByIds(ids, { ...opt, tx: this.tx });
1262
- }
1263
969
  }