@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.
@@ -2,18 +2,14 @@ import type { Transform } from 'node:stream'
2
2
  import { _isTruthy } from '@naturalcycles/js-lib'
3
3
  import { _uniqBy } from '@naturalcycles/js-lib/array/array.util.js'
4
4
  import { localTime } from '@naturalcycles/js-lib/datetime/localTime.js'
5
- import { _since } from '@naturalcycles/js-lib/datetime/time.util.js'
6
- import { _assert, AppError, ErrorMode } from '@naturalcycles/js-lib/error'
5
+ import { _assert, ErrorMode } from '@naturalcycles/js-lib/error'
7
6
  import type { JsonSchemaObject, JsonSchemaRootObject } from '@naturalcycles/js-lib/json-schema'
8
- import type { CommonLogger } from '@naturalcycles/js-lib/log'
9
7
  import { _deepJsonEquals } from '@naturalcycles/js-lib/object/deepEquals.js'
10
8
  import {
11
- _deepCopy,
12
9
  _filterUndefinedValues,
13
10
  _objectAssignExact,
14
11
  } from '@naturalcycles/js-lib/object/object.util.js'
15
12
  import { pMap } from '@naturalcycles/js-lib/promise/pMap.js'
16
- import { _truncate } from '@naturalcycles/js-lib/string/string.util.js'
17
13
  import {
18
14
  _stringMapEntries,
19
15
  _stringMapValues,
@@ -22,12 +18,15 @@ import {
22
18
  type NonNegativeInteger,
23
19
  type ObjectWithId,
24
20
  type StringMap,
25
- type UnixTimestampMillis,
26
21
  type Unsaved,
27
22
  } from '@naturalcycles/js-lib/types'
28
23
  import { _passthroughPredicate, _typeCast, SKIP } from '@naturalcycles/js-lib/types'
29
24
  import { stringId } from '@naturalcycles/nodejs-lib'
30
- import { type ReadableTyped, transformFlatten } from '@naturalcycles/nodejs-lib/stream'
25
+ import {
26
+ type ReadableTyped,
27
+ transformFlatten,
28
+ transformMapSync,
29
+ } from '@naturalcycles/nodejs-lib/stream'
31
30
  import {
32
31
  _pipeline,
33
32
  transformChunk,
@@ -37,7 +36,11 @@ import {
37
36
  writableVoid,
38
37
  } from '@naturalcycles/nodejs-lib/stream'
39
38
  import { DBLibError } from '../cnst.js'
40
- import type { CommonDBTransactionOptions, DBTransaction, RunQueryResult } from '../db.model.js'
39
+ import type {
40
+ CommonDBSaveOptions,
41
+ CommonDBTransactionOptions,
42
+ RunQueryResult,
43
+ } from '../db.model.js'
41
44
  import type { DBQuery } from '../query/dbQuery.js'
42
45
  import { RunnableDBQuery } from '../query/dbQuery.js'
43
46
  import type {
@@ -55,7 +58,7 @@ import type {
55
58
  CommonDaoStreamOptions,
56
59
  CommonDaoStreamSaveOptions,
57
60
  } from './common.dao.model.js'
58
- import { CommonDaoLogLevel } from './common.dao.model.js'
61
+ import { CommonDaoTransaction } from './commonDaoTransaction.js'
59
62
 
60
63
  /**
61
64
  * Lowest common denominator API between supported Databases.
@@ -71,7 +74,6 @@ export class CommonDao<
71
74
  > {
72
75
  constructor(public cfg: CommonDaoCfg<BM, DBM, ID>) {
73
76
  this.cfg = {
74
- logLevel: CommonDaoLogLevel.NONE,
75
77
  generateId: true,
76
78
  assignGeneratedIds: false,
77
79
  useCreatedProperty: true,
@@ -105,23 +107,14 @@ export class CommonDao<
105
107
  }
106
108
 
107
109
  // GET
108
- // overrides are disabled now, as they obfuscate errors when ID branded type is used
109
- // async getById(id: undefined | null, opt?: CommonDaoOptions): Promise<null>
110
- // async getById(id?: ID | null, opt?: CommonDaoOptions): Promise<BM | null>
111
- async getById(id?: ID | null, opt: CommonDaoReadOptions = {}): Promise<BM | null> {
112
- if (!id) return null
113
- const op = `getById(${id})`
114
- const table = opt.table || this.cfg.table
115
- const started = this.logStarted(op, table)
116
-
117
- let dbm = (await (opt.tx || this.cfg.db).getByIds<DBM>(table, [id as string], opt))[0]
118
- if (dbm && this.cfg.hooks!.afterLoad) {
119
- dbm = (await this.cfg.hooks!.afterLoad(dbm)) || undefined
120
- }
110
+ async requireById(id: ID, opt: CommonDaoReadOptions = {}): Promise<BM> {
111
+ const bm = await this.getById(id, opt)
112
+ return this.ensureRequired(bm, id, opt)
113
+ }
121
114
 
122
- const bm = await this.dbmToBM(dbm, opt)
123
- this.logResult(started, op, bm, table)
124
- return bm || null
115
+ async requireByIdAsDBM(id: ID, opt: CommonDaoReadOptions = {}): Promise<DBM> {
116
+ const dbm = await this.getByIdAsDBM(id, opt)
117
+ return this.ensureRequired(dbm, id, opt)
125
118
  }
126
119
 
127
120
  async getByIdOrEmpty(id: ID, part: Partial<BM> = {}, opt?: CommonDaoReadOptions): Promise<BM> {
@@ -131,111 +124,33 @@ export class CommonDao<
131
124
  return this.create({ ...part, id }, opt)
132
125
  }
133
126
 
134
- // async getByIdAsDBM(id: undefined | null, opt?: CommonDaoOptions): Promise<null>
135
- // async getByIdAsDBM(id?: ID | null, opt?: CommonDaoOptions): Promise<DBM | null>
136
- async getByIdAsDBM(id?: ID | null, opt: CommonDaoReadOptions = {}): Promise<DBM | null> {
127
+ async getById(id?: ID | null, opt: CommonDaoReadOptions = {}): Promise<BM | null> {
137
128
  if (!id) return null
138
- const op = `getByIdAsDBM(${id})`
139
- const table = opt.table || this.cfg.table
140
- const started = this.logStarted(op, table)
141
- let [dbm] = await (opt.tx || this.cfg.db).getByIds<DBM>(table, [id as string], opt)
142
- if (dbm && this.cfg.hooks!.afterLoad) {
143
- dbm = (await this.cfg.hooks!.afterLoad(dbm)) || undefined
144
- }
129
+ const [dbm] = await this.loadByIds([id], opt)
130
+ return await this.dbmToBM(dbm, opt)
131
+ }
145
132
 
146
- dbm = this.anyToDBM(dbm!, opt)
147
- this.logResult(started, op, dbm, table)
148
- return dbm || null
133
+ async getByIdAsDBM(id?: ID | null, opt: CommonDaoReadOptions = {}): Promise<DBM | null> {
134
+ if (!id) return null
135
+ const [row] = await this.loadByIds([id], opt)
136
+ return this.anyToDBM(row, opt) || null
149
137
  }
150
138
 
151
139
  async getByIds(ids: ID[], opt: CommonDaoReadOptions = {}): Promise<BM[]> {
152
- if (!ids.length) return []
153
- const op = `getByIds ${ids.length} id(s) (${_truncate(ids.slice(0, 10).join(', '), 50)})`
154
- const table = opt.table || this.cfg.table
155
- const started = this.logStarted(op, table)
156
- let dbms = await (opt.tx || this.cfg.db).getByIds<DBM>(table, ids as string[], opt)
157
- if (this.cfg.hooks!.afterLoad && dbms.length) {
158
- dbms = (await pMap(dbms, async dbm => await this.cfg.hooks!.afterLoad!(dbm))).filter(
159
- _isTruthy,
160
- )
161
- }
162
-
163
- const bms = await this.dbmsToBM(dbms, opt)
164
- this.logResult(started, op, bms, table)
165
- return bms
140
+ const dbms = await this.loadByIds(ids, opt)
141
+ return await this.dbmsToBM(dbms, opt)
166
142
  }
167
143
 
168
144
  async getByIdsAsDBM(ids: ID[], opt: CommonDaoReadOptions = {}): Promise<DBM[]> {
169
- if (!ids.length) return []
170
- const op = `getByIdsAsDBM ${ids.length} id(s) (${_truncate(ids.slice(0, 10).join(', '), 50)})`
171
- const table = opt.table || this.cfg.table
172
- const started = this.logStarted(op, table)
173
- let dbms = await (opt.tx || this.cfg.db).getByIds<DBM>(table, ids as string[], opt)
174
- if (this.cfg.hooks!.afterLoad && dbms.length) {
175
- dbms = (await pMap(dbms, async dbm => await this.cfg.hooks!.afterLoad!(dbm))).filter(
176
- _isTruthy,
177
- )
178
- }
179
-
180
- this.logResult(started, op, dbms, table)
181
- return dbms
182
- }
183
-
184
- async requireById(id: ID, opt: CommonDaoReadOptions = {}): Promise<BM> {
185
- const r = await this.getById(id, opt)
186
- if (!r) {
187
- this.throwRequiredError(id, opt)
188
- }
189
- return r
190
- }
191
-
192
- async requireByIdAsDBM(id: ID, opt: CommonDaoReadOptions = {}): Promise<DBM> {
193
- const r = await this.getByIdAsDBM(id, opt)
194
- if (!r) {
195
- this.throwRequiredError(id, opt)
196
- }
197
- return r
145
+ const rows = await this.loadByIds(ids, opt)
146
+ return this.anyToDBMs(rows)
198
147
  }
199
148
 
200
- private throwRequiredError(id: ID, opt: CommonDaoOptions): never {
149
+ // DRY private method
150
+ private async loadByIds(ids: ID[], opt: CommonDaoReadOptions = {}): Promise<DBM[]> {
151
+ if (!ids.length) return []
201
152
  const table = opt.table || this.cfg.table
202
- throw new AppError(`DB row required, but not found in ${table}`, {
203
- table,
204
- id,
205
- })
206
- }
207
-
208
- /**
209
- * Throws if readOnly is true
210
- */
211
- private requireWriteAccess(): void {
212
- if (this.cfg.readOnly) {
213
- throw new AppError(DBLibError.DAO_IS_READ_ONLY, {
214
- table: this.cfg.table,
215
- })
216
- }
217
- }
218
-
219
- /**
220
- * Throws if readOnly is true
221
- */
222
- private requireObjectMutability(opt: CommonDaoOptions): void {
223
- if (this.cfg.immutable && !opt.allowMutability) {
224
- throw new AppError(DBLibError.OBJECT_IS_IMMUTABLE, {
225
- table: this.cfg.table,
226
- })
227
- }
228
- }
229
-
230
- private async ensureUniqueId(table: string, dbm: DBM): Promise<void> {
231
- // todo: retry N times
232
- const existing = await this.cfg.db.getByIds<DBM>(table, [dbm.id])
233
- if (existing.length) {
234
- throw new AppError(DBLibError.NON_UNIQUE_ID, {
235
- table,
236
- ids: existing.map(i => i.id),
237
- })
238
- }
153
+ return await (opt.tx || this.cfg.db).getByIds<DBM>(table, ids, opt)
239
154
  }
240
155
 
241
156
  async getBy(by: keyof DBM, value: any, limit = 0, opt?: CommonDaoReadOptions): Promise<BM[]> {
@@ -294,18 +209,9 @@ export class CommonDao<
294
209
  ): Promise<RunQueryResult<BM>> {
295
210
  this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
296
211
  q.table = opt.table || q.table
297
- const op = `runQuery(${q.pretty()})`
298
- const started = this.logStarted(op, q.table)
299
- let { rows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
300
- const partialQuery = !!q._selectedFieldNames
301
- if (this.cfg.hooks!.afterLoad && rows.length) {
302
- rows = (await pMap(rows, async dbm => await this.cfg.hooks!.afterLoad!(dbm))).filter(
303
- _isTruthy,
304
- )
305
- }
306
-
307
- const bms = partialQuery ? (rows as any[]) : await this.dbmsToBM(rows, opt)
308
- this.logResult(started, op, bms, q.table)
212
+ const { rows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
213
+ const isPartialQuery = !!q._selectedFieldNames
214
+ const bms = isPartialQuery ? (rows as any[]) : await this.dbmsToBM(rows, opt)
309
215
  return {
310
216
  rows: bms,
311
217
  ...queryResult,
@@ -323,31 +229,16 @@ export class CommonDao<
323
229
  ): Promise<RunQueryResult<DBM>> {
324
230
  this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
325
231
  q.table = opt.table || q.table
326
- const op = `runQueryAsDBM(${q.pretty()})`
327
- const started = this.logStarted(op, q.table)
328
- let { rows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
329
- if (this.cfg.hooks!.afterLoad && rows.length) {
330
- rows = (await pMap(rows, async dbm => await this.cfg.hooks!.afterLoad!(dbm))).filter(
331
- _isTruthy,
332
- )
333
- }
334
-
335
- const partialQuery = !!q._selectedFieldNames
336
- const dbms = partialQuery ? rows : this.anyToDBMs(rows, opt)
337
- this.logResult(started, op, dbms, q.table)
232
+ const { rows, ...queryResult } = await this.cfg.db.runQuery<DBM>(q, opt)
233
+ const isPartialQuery = !!q._selectedFieldNames
234
+ const dbms = isPartialQuery ? rows : this.anyToDBMs(rows, opt)
338
235
  return { rows: dbms, ...queryResult }
339
236
  }
340
237
 
341
238
  async runQueryCount(q: DBQuery<DBM>, opt: CommonDaoReadOptions = {}): Promise<number> {
342
239
  this.validateQueryIndexes(q) // throws if query uses `excludeFromIndexes` property
343
240
  q.table = opt.table || q.table
344
- const op = `runQueryCount(${q.pretty()})`
345
- const started = this.logStarted(op, q.table)
346
- const count = await this.cfg.db.runQueryCount(q, opt)
347
- if (this.cfg.logLevel! >= CommonDaoLogLevel.OPERATIONS) {
348
- this.cfg.logger?.log(`<< ${q.table}.${op}: ${count} row(s) in ${_since(started)}`)
349
- }
350
- return count
241
+ return await this.cfg.db.runQueryCount(q, opt)
351
242
  }
352
243
 
353
244
  async streamQueryForEach(
@@ -360,23 +251,13 @@ export class CommonDao<
360
251
  opt.skipValidation = opt.skipValidation !== false // default true
361
252
  opt.errorMode ||= ErrorMode.SUPPRESS
362
253
 
363
- const partialQuery = !!q._selectedFieldNames
364
- const op = `streamQueryForEach(${q.pretty()})`
365
- const started = this.logStarted(op, q.table, true)
366
- let count = 0
254
+ const isPartialQuery = !!q._selectedFieldNames
367
255
 
368
256
  await _pipeline([
369
257
  this.cfg.db.streamQuery<DBM>(q, opt),
370
258
  transformMap<DBM, BM>(
371
259
  async dbm => {
372
- count++
373
- if (partialQuery) return dbm as any
374
-
375
- if (this.cfg.hooks!.afterLoad) {
376
- dbm = (await this.cfg.hooks!.afterLoad(dbm))!
377
- if (dbm === null) return SKIP
378
- }
379
-
260
+ if (isPartialQuery) return dbm as any
380
261
  return await this.dbmToBM(dbm, opt)
381
262
  },
382
263
  {
@@ -394,10 +275,6 @@ export class CommonDao<
394
275
  }),
395
276
  writableVoid(),
396
277
  ])
397
-
398
- if (this.cfg.logLevel! >= CommonDaoLogLevel.OPERATIONS) {
399
- this.cfg.logger?.log(`<< ${q.table}.${op}: ${count} row(s) in ${_since(started)}`)
400
- }
401
278
  }
402
279
 
403
280
  async streamQueryAsDBMForEach(
@@ -410,23 +287,13 @@ export class CommonDao<
410
287
  opt.skipValidation = opt.skipValidation !== false // default true
411
288
  opt.errorMode ||= ErrorMode.SUPPRESS
412
289
 
413
- const partialQuery = !!q._selectedFieldNames
414
- const op = `streamQueryAsDBMForEach(${q.pretty()})`
415
- const started = this.logStarted(op, q.table, true)
416
- let count = 0
290
+ const isPartialQuery = !!q._selectedFieldNames
417
291
 
418
292
  await _pipeline([
419
293
  this.cfg.db.streamQuery<any>(q, opt),
420
- transformMap<any, DBM>(
421
- async dbm => {
422
- count++
423
- if (partialQuery) return dbm
424
-
425
- if (this.cfg.hooks!.afterLoad) {
426
- dbm = (await this.cfg.hooks!.afterLoad(dbm))!
427
- if (dbm === null) return SKIP
428
- }
429
-
294
+ transformMapSync<any, DBM>(
295
+ dbm => {
296
+ if (isPartialQuery) return dbm
430
297
  return this.anyToDBM(dbm, opt)
431
298
  },
432
299
  {
@@ -444,10 +311,6 @@ export class CommonDao<
444
311
  }),
445
312
  writableVoid(),
446
313
  ])
447
-
448
- if (this.cfg.logLevel! >= CommonDaoLogLevel.OPERATIONS) {
449
- this.cfg.logger?.log(`<< ${q.table}.${op}: ${count} row(s) in ${_since(started)}`)
450
- }
451
314
  }
452
315
 
453
316
  /**
@@ -459,23 +322,18 @@ export class CommonDao<
459
322
  opt.skipValidation = opt.skipValidation !== false // default true
460
323
  opt.errorMode ||= ErrorMode.SUPPRESS
461
324
 
462
- const partialQuery = !!q._selectedFieldNames
325
+ const isPartialQuery = !!q._selectedFieldNames
463
326
 
464
327
  const stream = this.cfg.db.streamQuery<DBM>(q, opt)
465
- if (partialQuery) return stream
328
+ if (isPartialQuery) return stream
466
329
 
467
330
  return (
468
331
  stream
469
332
  // the commented out line was causing RangeError: Maximum call stack size exceeded
470
333
  // .on('error', err => stream.emit('error', err))
471
334
  .pipe(
472
- transformMap<any, DBM>(
473
- async dbm => {
474
- if (this.cfg.hooks!.afterLoad) {
475
- dbm = (await this.cfg.hooks!.afterLoad(dbm))!
476
- if (dbm === null) return SKIP
477
- }
478
-
335
+ transformMapSync<any, DBM>(
336
+ dbm => {
479
337
  return this.anyToDBM(dbm, opt)
480
338
  },
481
339
  {
@@ -502,16 +360,11 @@ export class CommonDao<
502
360
  opt.errorMode ||= ErrorMode.SUPPRESS
503
361
 
504
362
  const stream = this.cfg.db.streamQuery<DBM>(q, opt)
505
- const partialQuery = !!q._selectedFieldNames
506
- if (partialQuery) return stream as any
363
+ const isPartialQuery = !!q._selectedFieldNames
364
+ if (isPartialQuery) return stream as any
507
365
 
508
366
  // This almost works, but hard to implement `errorMode: THROW_AGGREGATED` in this case
509
367
  // return stream.flatMap(async (dbm: DBM) => {
510
- // if (this.cfg.hooks!.afterLoad) {
511
- // dbm = (await this.cfg.hooks!.afterLoad(dbm))!
512
- // if (dbm === null) return [] // SKIP
513
- // }
514
- //
515
368
  // return [await this.dbmToBM(dbm, opt)] satisfies BM[]
516
369
  // }, {
517
370
  // concurrency: 16,
@@ -527,11 +380,6 @@ export class CommonDao<
527
380
  .pipe(
528
381
  transformMap<DBM, BM>(
529
382
  async dbm => {
530
- if (this.cfg.hooks!.afterLoad) {
531
- dbm = (await this.cfg.hooks!.afterLoad(dbm))!
532
- if (dbm === null) return SKIP
533
- }
534
-
535
383
  return await this.dbmToBM(dbm, opt)
536
384
  },
537
385
  {
@@ -586,15 +434,8 @@ export class CommonDao<
586
434
  q.table = opt.table || q.table
587
435
  opt.errorMode ||= ErrorMode.SUPPRESS
588
436
 
589
- const op = `streamQueryIdsForEach(${q.pretty()})`
590
- const started = this.logStarted(op, q.table, true)
591
- let count = 0
592
-
593
437
  await _pipeline([
594
- this.cfg.db.streamQuery<DBM>(q.select(['id']), opt).map(r => {
595
- count++
596
- return r.id
597
- }),
438
+ this.cfg.db.streamQuery<DBM>(q.select(['id']), opt).map(r => r.id),
598
439
  transformMap<ID, void>(mapper, {
599
440
  ...opt,
600
441
  predicate: _passthroughPredicate,
@@ -606,17 +447,15 @@ export class CommonDao<
606
447
  }),
607
448
  writableVoid(),
608
449
  ])
609
-
610
- if (this.cfg.logLevel! >= CommonDaoLogLevel.OPERATIONS) {
611
- this.cfg.logger?.log(`<< ${q.table}.${op}: ${count} id(s) in ${_since(started)}`)
612
- }
613
450
  }
614
451
 
615
452
  /**
616
453
  * Mutates!
617
- * "Returns", just to have a type of "Saved"
618
454
  */
619
- assignIdCreatedUpdated<T extends BaseDBEntity>(obj: Partial<T>, opt: CommonDaoOptions = {}): T {
455
+ assignIdCreatedUpdated<T extends BaseDBEntity>(
456
+ obj: Partial<T>,
457
+ opt: CommonDaoOptions = {},
458
+ ): void {
620
459
  const now = localTime.nowUnix()
621
460
 
622
461
  if (this.cfg.useCreatedProperty) {
@@ -631,8 +470,6 @@ export class CommonDao<
631
470
  obj.id ||= (this.cfg.hooks!.createNaturalId?.(obj as any) ||
632
471
  this.cfg.hooks!.createRandomId!()) as T['id']
633
472
  }
634
-
635
- return obj as T
636
473
  }
637
474
 
638
475
  // SAVE
@@ -777,7 +614,6 @@ export class CommonDao<
777
614
  }
778
615
  }
779
616
 
780
- const idWasGenerated = !bm.id && this.cfg.generateId
781
617
  this.assignIdCreatedUpdated(bm, opt) // mutates
782
618
  _typeCast<BM>(bm)
783
619
  let dbm = await this.bmToDBM(bm, opt) // validates BM
@@ -788,71 +624,43 @@ export class CommonDao<
788
624
  }
789
625
 
790
626
  const table = opt.table || this.cfg.table
791
- if (opt.ensureUniqueId && idWasGenerated) await this.ensureUniqueId(table, dbm)
792
- if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
793
- opt = { ...opt, saveMethod: 'insert' }
794
- }
795
- const op = `save(${dbm.id})`
796
- const started = this.logSaveStarted(op, bm, table)
797
- const { excludeFromIndexes } = this.cfg
798
- const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds
627
+ const saveOptions = this.prepareSaveOptions(opt)
799
628
 
800
- await (opt.tx || this.cfg.db).saveBatch(table, [dbm], {
801
- excludeFromIndexes,
802
- assignGeneratedIds,
803
- ...opt,
804
- })
629
+ await (opt.tx || this.cfg.db).saveBatch(table, [dbm], saveOptions)
805
630
 
806
- if (assignGeneratedIds) {
631
+ if (saveOptions.assignGeneratedIds) {
807
632
  bm.id = dbm.id
808
633
  }
809
634
 
810
- this.logSaveResult(started, op, table)
811
635
  return bm
812
636
  }
813
637
 
814
638
  async saveAsDBM(dbm: Unsaved<DBM>, opt: CommonDaoSaveOptions<BM, DBM> = {}): Promise<DBM> {
815
639
  this.requireWriteAccess()
816
- const table = opt.table || this.cfg.table
817
640
 
818
- // assigning id in case it misses the id
819
- // will override/set `updated` field, unless opts.preserveUpdated is set
820
- const idWasGenerated = !dbm.id && this.cfg.generateId
821
641
  this.assignIdCreatedUpdated(dbm, opt) // mutates
822
642
  let row = this.anyToDBM(dbm, opt)
823
- if (opt.ensureUniqueId && idWasGenerated) await this.ensureUniqueId(table, row)
824
-
825
- if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
826
- opt = { ...opt, saveMethod: 'insert' }
827
- }
828
- const op = `saveAsDBM(${row.id})`
829
- const started = this.logSaveStarted(op, row, table)
830
- const { excludeFromIndexes } = this.cfg
831
- const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds
832
643
 
833
644
  if (this.cfg.hooks!.beforeSave) {
834
645
  row = (await this.cfg.hooks!.beforeSave(row))!
835
646
  if (row === null) return dbm as DBM
836
647
  }
837
648
 
838
- await (opt.tx || this.cfg.db).saveBatch(table, [row], {
839
- excludeFromIndexes,
840
- assignGeneratedIds,
841
- ...opt,
842
- })
649
+ const table = opt.table || this.cfg.table
650
+ const saveOptions = this.prepareSaveOptions(opt)
651
+
652
+ await (opt.tx || this.cfg.db).saveBatch(table, [row], saveOptions)
843
653
 
844
- if (assignGeneratedIds) {
654
+ if (saveOptions.assignGeneratedIds) {
845
655
  dbm.id = row.id
846
656
  }
847
657
 
848
- this.logSaveResult(started, op, table)
849
658
  return row
850
659
  }
851
660
 
852
661
  async saveBatch(bms: Unsaved<BM>[], opt: CommonDaoSaveBatchOptions<DBM> = {}): Promise<BM[]> {
853
662
  if (!bms.length) return []
854
663
  this.requireWriteAccess()
855
- const table = opt.table || this.cfg.table
856
664
  bms.forEach(bm => this.assignIdCreatedUpdated(bm, opt))
857
665
  let dbms = await this.bmsToDBM(bms as BM[], opt)
858
666
 
@@ -862,34 +670,15 @@ export class CommonDao<
862
670
  )
863
671
  }
864
672
 
865
- if (opt.ensureUniqueId) throw new AppError('ensureUniqueId is not supported in saveBatch')
866
- if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
867
- opt = { ...opt, saveMethod: 'insert' }
868
- }
869
-
870
- const op = `saveBatch ${dbms.length} row(s) (${_truncate(
871
- dbms
872
- .slice(0, 10)
873
- .map(bm => bm.id)
874
- .join(', '),
875
- 50,
876
- )})`
877
- const started = this.logSaveStarted(op, bms, table)
878
- const { excludeFromIndexes } = this.cfg
879
- const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds
673
+ const table = opt.table || this.cfg.table
674
+ const saveOptions = this.prepareSaveOptions(opt)
880
675
 
881
- await (opt.tx || this.cfg.db).saveBatch(table, dbms, {
882
- excludeFromIndexes,
883
- assignGeneratedIds,
884
- ...opt,
885
- })
676
+ await (opt.tx || this.cfg.db).saveBatch(table, dbms, saveOptions)
886
677
 
887
- if (assignGeneratedIds) {
678
+ if (saveOptions.assignGeneratedIds) {
888
679
  dbms.forEach((dbm, i) => (bms[i]!.id = dbm.id))
889
680
  }
890
681
 
891
- this.logSaveResult(started, op, table)
892
-
893
682
  return bms as BM[]
894
683
  }
895
684
 
@@ -899,24 +688,8 @@ export class CommonDao<
899
688
  ): Promise<DBM[]> {
900
689
  if (!dbms.length) return []
901
690
  this.requireWriteAccess()
902
- const table = opt.table || this.cfg.table
903
- dbms.forEach(dbm => this.assignIdCreatedUpdated(dbm, opt)) // mutates
691
+ dbms.forEach(dbm => this.assignIdCreatedUpdated(dbm, opt))
904
692
  let rows = this.anyToDBMs(dbms as DBM[], opt)
905
- if (opt.ensureUniqueId) throw new AppError('ensureUniqueId is not supported in saveBatch')
906
-
907
- if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
908
- opt = { ...opt, saveMethod: 'insert' }
909
- }
910
- const op = `saveBatchAsDBM ${rows.length} row(s) (${_truncate(
911
- rows
912
- .slice(0, 10)
913
- .map(bm => bm.id)
914
- .join(', '),
915
- 50,
916
- )})`
917
- const started = this.logSaveStarted(op, rows, table)
918
- const { excludeFromIndexes } = this.cfg
919
- const assignGeneratedIds = opt.assignGeneratedIds || this.cfg.assignGeneratedIds
920
693
 
921
694
  if (this.cfg.hooks!.beforeSave && rows.length) {
922
695
  rows = (await pMap(rows, async row => await this.cfg.hooks!.beforeSave!(row))).filter(
@@ -924,20 +697,36 @@ export class CommonDao<
924
697
  )
925
698
  }
926
699
 
927
- await (opt.tx || this.cfg.db).saveBatch(table, rows, {
928
- excludeFromIndexes,
929
- assignGeneratedIds,
930
- ...opt,
931
- })
700
+ const table = opt.table || this.cfg.table
701
+ const saveOptions = this.prepareSaveOptions(opt)
932
702
 
933
- if (assignGeneratedIds) {
703
+ await (opt.tx || this.cfg.db).saveBatch(table, rows, saveOptions)
704
+
705
+ if (saveOptions.assignGeneratedIds) {
934
706
  rows.forEach((row, i) => (dbms[i]!.id = row.id))
935
707
  }
936
708
 
937
- this.logSaveResult(started, op, table)
938
709
  return rows
939
710
  }
940
711
 
712
+ private prepareSaveOptions(opt: CommonDaoSaveOptions<BM, DBM>): CommonDBSaveOptions<DBM> {
713
+ let {
714
+ saveMethod,
715
+ assignGeneratedIds = this.cfg.assignGeneratedIds,
716
+ excludeFromIndexes = this.cfg.excludeFromIndexes,
717
+ } = opt
718
+ if (this.cfg.immutable && !opt.allowMutability && !opt.saveMethod) {
719
+ saveMethod = 'insert'
720
+ }
721
+
722
+ return {
723
+ ...opt,
724
+ excludeFromIndexes,
725
+ saveMethod,
726
+ assignGeneratedIds,
727
+ }
728
+ }
729
+
941
730
  /**
942
731
  * "Streaming" is implemented by buffering incoming rows into **batches**
943
732
  * (of size opt.chunkSize, which defaults to 500),
@@ -1016,12 +805,8 @@ export class CommonDao<
1016
805
  if (!ids.length) return 0
1017
806
  this.requireWriteAccess()
1018
807
  this.requireObjectMutability(opt)
1019
- const op = `deleteByIds(${ids.join(', ')})`
1020
808
  const table = opt.table || this.cfg.table
1021
- const started = this.logStarted(op, table)
1022
- const count = await (opt.tx || this.cfg.db).deleteByIds(table, ids as string[], opt)
1023
- this.logSaveResult(started, op, table)
1024
- return count
809
+ return await (opt.tx || this.cfg.db).deleteByIds(table, ids, opt)
1025
810
  }
1026
811
 
1027
812
  /**
@@ -1037,8 +822,6 @@ export class CommonDao<
1037
822
  this.requireWriteAccess()
1038
823
  this.requireObjectMutability(opt)
1039
824
  q.table = opt.table || q.table
1040
- const op = `deleteByQuery(${q.pretty()})`
1041
- const started = this.logStarted(op, q.table)
1042
825
  let deleted = 0
1043
826
 
1044
827
  if (opt.chunkSize) {
@@ -1071,7 +854,6 @@ export class CommonDao<
1071
854
  deleted = await this.cfg.db.deleteByQuery(q, opt)
1072
855
  }
1073
856
 
1074
- this.logSaveResult(started, op, q.table)
1075
857
  return deleted
1076
858
  }
1077
859
 
@@ -1089,11 +871,7 @@ export class CommonDao<
1089
871
  this.requireWriteAccess()
1090
872
  this.requireObjectMutability(opt)
1091
873
  q.table = opt.table || q.table
1092
- const op = `patchByQuery(${q.pretty()})`
1093
- const started = this.logStarted(op, q.table)
1094
- const updated = await this.cfg.db.patchByQuery(q, patch, opt)
1095
- this.logSaveResult(started, op, q.table)
1096
- return updated
874
+ return await this.cfg.db.patchByQuery(q, patch, opt)
1097
875
  }
1098
876
 
1099
877
  /**
@@ -1105,13 +883,10 @@ export class CommonDao<
1105
883
  this.requireWriteAccess()
1106
884
  this.requireObjectMutability(opt)
1107
885
  const { table } = this.cfg
1108
- const op = `increment`
1109
- const started = this.logStarted(op, table)
1110
886
  const result = await this.cfg.db.incrementBatch(table, prop as string, {
1111
- [id as string]: by,
887
+ [id]: by,
1112
888
  })
1113
- this.logSaveResult(started, op, table)
1114
- return result[id as string]!
889
+ return result[id]!
1115
890
  }
1116
891
 
1117
892
  /**
@@ -1127,19 +902,15 @@ export class CommonDao<
1127
902
  this.requireWriteAccess()
1128
903
  this.requireObjectMutability(opt)
1129
904
  const { table } = this.cfg
1130
- const op = `incrementBatch`
1131
- const started = this.logStarted(op, table)
1132
- const result = await this.cfg.db.incrementBatch(table, prop as string, incrementMap)
1133
- this.logSaveResult(started, op, table)
1134
- return result
905
+ return await this.cfg.db.incrementBatch(table, prop as string, incrementMap)
1135
906
  }
1136
907
 
1137
908
  // CONVERSIONS
1138
909
 
1139
- async dbmToBM(_dbm: undefined, opt?: CommonDaoOptions): Promise<undefined>
910
+ async dbmToBM(_dbm: undefined, opt?: CommonDaoOptions): Promise<null>
1140
911
  async dbmToBM(_dbm?: DBM, opt?: CommonDaoOptions): Promise<BM>
1141
- async dbmToBM(_dbm?: DBM, opt: CommonDaoOptions = {}): Promise<BM | undefined> {
1142
- if (!_dbm) return
912
+ async dbmToBM(_dbm?: DBM, opt: CommonDaoOptions = {}): Promise<BM | null> {
913
+ if (!_dbm) return null
1143
914
 
1144
915
  // optimization: no need to run full joi DBM validation, cause BM validation will be run
1145
916
  // const dbm = this.anyToDBM(_dbm, opt)
@@ -1164,10 +935,10 @@ export class CommonDao<
1164
935
  * Mutates object with properties: id, created, updated.
1165
936
  * Returns DBM (new reference).
1166
937
  */
1167
- async bmToDBM(bm: undefined, opt?: CommonDaoOptions): Promise<undefined>
938
+ async bmToDBM(bm: undefined, opt?: CommonDaoOptions): Promise<null>
1168
939
  async bmToDBM(bm?: BM, opt?: CommonDaoOptions): Promise<DBM>
1169
- async bmToDBM(bm?: BM, opt?: CommonDaoOptions): Promise<DBM | undefined> {
1170
- if (bm === undefined) return
940
+ async bmToDBM(bm?: BM, opt?: CommonDaoOptions): Promise<DBM | null> {
941
+ if (bm === undefined) return null
1171
942
 
1172
943
  // bm gets assigned to the new reference
1173
944
  bm = this.validateAndConvert(bm, 'save', opt)
@@ -1181,10 +952,10 @@ export class CommonDao<
1181
952
  return await pMap(bms, async bm => await this.bmToDBM(bm, opt))
1182
953
  }
1183
954
 
1184
- anyToDBM(dbm: undefined, opt?: CommonDaoOptions): undefined
955
+ anyToDBM(dbm: undefined, opt?: CommonDaoOptions): null
1185
956
  anyToDBM(dbm?: any, opt?: CommonDaoOptions): DBM
1186
- anyToDBM(dbm?: DBM, opt: CommonDaoOptions = {}): DBM | undefined {
1187
- if (!dbm) return
957
+ anyToDBM(dbm?: DBM, opt: CommonDaoOptions = {}): DBM | null {
958
+ if (!dbm) return null
1188
959
 
1189
960
  // this shouldn't be happening on load! but should on save!
1190
961
  // this.assignIdCreatedUpdated(dbm, opt)
@@ -1202,8 +973,8 @@ export class CommonDao<
1202
973
  return dbm
1203
974
  }
1204
975
 
1205
- anyToDBMs(entities: DBM[], opt: CommonDaoOptions = {}): DBM[] {
1206
- return entities.map(entity => this.anyToDBM(entity, opt))
976
+ anyToDBMs(rows: DBM[], opt: CommonDaoOptions = {}): DBM[] {
977
+ return rows.map(entity => this.anyToDBM(entity, opt))
1207
978
  }
1208
979
 
1209
980
  /**
@@ -1278,10 +1049,18 @@ export class CommonDao<
1278
1049
  }
1279
1050
  }
1280
1051
 
1281
- toSave(input: BM | BM[]): DaoWithRows<typeof this> {
1052
+ withRows(rows: BM[]): DaoWithRows<typeof this> {
1053
+ return {
1054
+ dao: this,
1055
+ rows: rows as any,
1056
+ }
1057
+ }
1058
+
1059
+ withRow(row: BM, opt?: DaoWithRowOptions<BM>): DaoWithRow<typeof this> {
1282
1060
  return {
1283
1061
  dao: this,
1284
- rows: [input].flat() as any[],
1062
+ row: row as any,
1063
+ opt: opt as any,
1285
1064
  }
1286
1065
  }
1287
1066
 
@@ -1402,16 +1181,39 @@ export class CommonDao<
1402
1181
  }
1403
1182
 
1404
1183
  static async multiSave(
1405
- inputs: DaoWithRows<any>[],
1184
+ inputs: (DaoWithRows<any> | DaoWithRow<any>)[],
1406
1185
  opt: CommonDaoSaveBatchOptions<any> = {},
1407
1186
  ): Promise<void> {
1408
1187
  if (!inputs.length) return
1409
1188
  const { db } = inputs[0]!.dao.cfg
1410
1189
  const dbmsByTable: StringMap<any[]> = {}
1411
- await pMap(inputs, async ({ dao, rows }) => {
1190
+ await pMap(inputs, async input => {
1191
+ const { dao } = input
1412
1192
  const { table } = dao.cfg
1413
- rows.forEach(bm => dao.assignIdCreatedUpdated(bm, opt))
1414
- dbmsByTable[table] = await dao.bmsToDBM(rows, opt)
1193
+ dbmsByTable[table] ||= []
1194
+
1195
+ if ('row' in input) {
1196
+ // Singular
1197
+ const { row } = input
1198
+
1199
+ if (input.opt?.skipIfEquals) {
1200
+ // We compare with convertedBM, to account for cases when some extra property is assigned to bm,
1201
+ // which should be removed post-validation, but it breaks the "equality check"
1202
+ // Post-validation the equality check should work as intended
1203
+ const convertedBM = dao.validateAndConvert(row, 'save', opt)
1204
+ if (_deepJsonEquals(convertedBM, input.opt.skipIfEquals)) {
1205
+ // Skipping the save operation
1206
+ return
1207
+ }
1208
+ }
1209
+
1210
+ dao.assignIdCreatedUpdated(row, opt)
1211
+ dbmsByTable[table].push(await dao.bmToDBM(row, opt))
1212
+ } else {
1213
+ // Plural
1214
+ input.rows.forEach(bm => dao.assignIdCreatedUpdated(bm, opt))
1215
+ dbmsByTable[table].push(...(await dao.bmsToDBM(input.rows, opt)))
1216
+ }
1415
1217
  })
1416
1218
 
1417
1219
  await db.multiSave(dbmsByTable)
@@ -1442,6 +1244,33 @@ export class CommonDao<
1442
1244
  return r!
1443
1245
  }
1444
1246
 
1247
+ private ensureRequired<ROW>(row: ROW, id: string, opt: CommonDaoOptions): NonNullable<ROW> {
1248
+ const table = opt.table || this.cfg.table
1249
+ _assert(row, `DB row required, but not found in ${table}`, {
1250
+ table,
1251
+ id,
1252
+ })
1253
+ return row // pass-through
1254
+ }
1255
+
1256
+ /**
1257
+ * Throws if readOnly is true
1258
+ */
1259
+ private requireWriteAccess(): void {
1260
+ _assert(!this.cfg.readOnly, DBLibError.DAO_IS_READ_ONLY, {
1261
+ table: this.cfg.table,
1262
+ })
1263
+ }
1264
+
1265
+ /**
1266
+ * Throws if readOnly is true
1267
+ */
1268
+ private requireObjectMutability(opt: CommonDaoOptions): void {
1269
+ _assert(!this.cfg.immutable || opt.allowMutability, DBLibError.OBJECT_IS_IMMUTABLE, {
1270
+ table: this.cfg.table,
1271
+ })
1272
+ }
1273
+
1445
1274
  /**
1446
1275
  * Throws if query uses a property that is in `excludeFromIndexes` list.
1447
1276
  */
@@ -1459,62 +1288,6 @@ export class CommonDao<
1459
1288
  )
1460
1289
  }
1461
1290
  }
1462
-
1463
- protected logResult(started: UnixTimestampMillis, op: string, res: any, table: string): void {
1464
- if (!this.cfg.logLevel) return
1465
-
1466
- let logRes: any
1467
- const args: any[] = []
1468
-
1469
- if (Array.isArray(res)) {
1470
- logRes = `${res.length} row(s)`
1471
- if (res.length && this.cfg.logLevel >= CommonDaoLogLevel.DATA_FULL) {
1472
- args.push('\n', ...res.slice(0, 10)) // max 10 items
1473
- }
1474
- } else if (res) {
1475
- logRes = `1 row`
1476
- if (this.cfg.logLevel >= CommonDaoLogLevel.DATA_SINGLE) {
1477
- args.push('\n', res)
1478
- }
1479
- } else {
1480
- logRes = `undefined`
1481
- }
1482
-
1483
- this.cfg.logger?.log(`<< ${table}.${op}: ${logRes} in ${_since(started)}`, ...args)
1484
- }
1485
-
1486
- protected logSaveResult(started: UnixTimestampMillis, op: string, table: string): void {
1487
- if (!this.cfg.logLevel) return
1488
- this.cfg.logger?.log(`<< ${table}.${op} in ${_since(started)}`)
1489
- }
1490
-
1491
- protected logStarted(op: string, table: string, force = false): UnixTimestampMillis {
1492
- if (this.cfg.logStarted || force) {
1493
- this.cfg.logger?.log(`>> ${table}.${op}`)
1494
- }
1495
- return localTime.nowUnixMillis()
1496
- }
1497
-
1498
- protected logSaveStarted(op: string, items: any, table: string): UnixTimestampMillis {
1499
- if (this.cfg.logStarted) {
1500
- const args: any[] = [`>> ${table}.${op}`]
1501
- if (Array.isArray(items)) {
1502
- if (items.length && this.cfg.logLevel! >= CommonDaoLogLevel.DATA_FULL) {
1503
- args.push('\n', ...items.slice(0, 10))
1504
- } else {
1505
- args.push(`${items.length} row(s)`)
1506
- }
1507
- } else {
1508
- if (this.cfg.logLevel! >= CommonDaoLogLevel.DATA_SINGLE) {
1509
- args.push(items)
1510
- }
1511
- }
1512
-
1513
- this.cfg.logger?.log(...args)
1514
- }
1515
-
1516
- return localTime.nowUnixMillis()
1517
- }
1518
1291
  }
1519
1292
 
1520
1293
  /**
@@ -1524,120 +1297,6 @@ export class CommonDao<
1524
1297
  */
1525
1298
  export type CommonDaoTransactionFn<T = void> = (tx: CommonDaoTransaction) => Promise<T>
1526
1299
 
1527
- /**
1528
- * Transaction context.
1529
- * Has similar API than CommonDao, but all operations are performed in the context of the transaction.
1530
- */
1531
- export class CommonDaoTransaction {
1532
- constructor(
1533
- public tx: DBTransaction,
1534
- private logger: CommonLogger,
1535
- ) {}
1536
-
1537
- /**
1538
- * Commits the underlying DBTransaction.
1539
- * May throw.
1540
- */
1541
- async commit(): Promise<void> {
1542
- await this.tx.commit()
1543
- }
1544
-
1545
- /**
1546
- * Perform a graceful rollback without throwing/re-throwing any error.
1547
- * Never throws.
1548
- */
1549
- async rollback(): Promise<void> {
1550
- try {
1551
- await this.tx.rollback()
1552
- } catch (err) {
1553
- // graceful rollback without re-throw
1554
- this.logger.error(err)
1555
- }
1556
- }
1557
-
1558
- async getById<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID extends string = BM['id']>(
1559
- dao: CommonDao<BM, DBM, ID>,
1560
- id?: ID | null,
1561
- opt?: CommonDaoReadOptions,
1562
- ): Promise<BM | null> {
1563
- return await dao.getById(id, { ...opt, tx: this.tx })
1564
- }
1565
-
1566
- async getByIds<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID extends string = BM['id']>(
1567
- dao: CommonDao<BM, DBM, ID>,
1568
- ids: ID[],
1569
- opt?: CommonDaoReadOptions,
1570
- ): Promise<BM[]> {
1571
- return await dao.getByIds(ids, { ...opt, tx: this.tx })
1572
- }
1573
-
1574
- // todo: Queries inside Transaction are not supported yet
1575
- // async runQuery<BM extends PartialObjectWithId, DBM extends ObjectWithId>(
1576
- // dao: CommonDao<BM, DBM, any>,
1577
- // q: DBQuery<DBM>,
1578
- // opt?: CommonDaoOptions,
1579
- // ): Promise<BM[]> {
1580
- // try {
1581
- // return await dao.runQuery(q, { ...opt, tx: this.tx })
1582
- // } catch (err) {
1583
- // await this.rollback()
1584
- // throw err
1585
- // }
1586
- // }
1587
-
1588
- async save<BM extends BaseDBEntity, DBM extends BaseDBEntity>(
1589
- dao: CommonDao<BM, DBM>,
1590
- bm: Unsaved<BM>,
1591
- opt?: CommonDaoSaveOptions<BM, DBM>,
1592
- ): Promise<BM> {
1593
- return await dao.save(bm, { ...opt, tx: this.tx })
1594
- }
1595
-
1596
- async saveBatch<BM extends BaseDBEntity, DBM extends BaseDBEntity>(
1597
- dao: CommonDao<BM, DBM>,
1598
- bms: Unsaved<BM>[],
1599
- opt?: CommonDaoSaveBatchOptions<DBM>,
1600
- ): Promise<BM[]> {
1601
- return await dao.saveBatch(bms, { ...opt, tx: this.tx })
1602
- }
1603
-
1604
- /**
1605
- * DaoTransaction.patch does not load from DB.
1606
- * It assumes the bm was previously loaded in the same Transaction, hence could not be
1607
- * concurrently modified. Hence it's safe to not sync with DB.
1608
- *
1609
- * So, this method is a rather simple convenience "Object.assign and then save".
1610
- */
1611
- async patch<BM extends BaseDBEntity, DBM extends BaseDBEntity, ID extends string = BM['id']>(
1612
- dao: CommonDao<BM, DBM, ID>,
1613
- bm: BM,
1614
- patch: Partial<BM>,
1615
- opt?: CommonDaoSaveOptions<BM, DBM>,
1616
- ): Promise<BM> {
1617
- const skipIfEquals = _deepCopy(bm)
1618
- Object.assign(bm, patch)
1619
- return await dao.save(bm, { ...opt, skipIfEquals, tx: this.tx })
1620
- }
1621
-
1622
- // todo: use AnyDao/Infer in other methods as well, if this works well
1623
- async deleteById<DAO extends AnyDao>(
1624
- dao: DAO,
1625
- id?: InferID<DAO> | null,
1626
- opt?: CommonDaoOptions,
1627
- ): Promise<number> {
1628
- if (!id) return 0
1629
- return await this.deleteByIds(dao, [id], opt)
1630
- }
1631
-
1632
- async deleteByIds<DAO extends AnyDao>(
1633
- dao: DAO,
1634
- ids: InferID<DAO>[],
1635
- opt?: CommonDaoOptions,
1636
- ): Promise<number> {
1637
- return await dao.deleteByIds(ids, { ...opt, tx: this.tx })
1638
- }
1639
- }
1640
-
1641
1300
  export interface DaoWithIds<DAO extends AnyDao> {
1642
1301
  dao: DAO
1643
1302
  ids: string[]
@@ -1653,8 +1312,18 @@ export interface DaoWithRows<DAO extends AnyDao> {
1653
1312
  rows: InferBM<DAO>[]
1654
1313
  }
1655
1314
 
1656
- type InferBM<DAO> = DAO extends CommonDao<infer BM> ? BM : never
1657
- // type InferDBM<DAO> = DAO extends CommonDao<any, infer DBM> ? DBM : never
1658
- type InferID<DAO> = DAO extends CommonDao<any, any, infer ID> ? ID : never
1315
+ export interface DaoWithRow<DAO extends AnyDao> {
1316
+ dao: DAO
1317
+ row: InferBM<DAO>
1318
+ opt?: DaoWithRowOptions<InferBM<DAO>>
1319
+ }
1320
+
1321
+ interface DaoWithRowOptions<BM extends BaseDBEntity> {
1322
+ skipIfEquals?: BM
1323
+ }
1324
+
1325
+ export type InferBM<DAO> = DAO extends CommonDao<infer BM> ? BM : never
1326
+ export type InferDBM<DAO> = DAO extends CommonDao<any, infer DBM> ? DBM : never
1327
+ export type InferID<DAO> = DAO extends CommonDao<any, any, infer ID> ? ID : never
1659
1328
 
1660
1329
  export type AnyDao = CommonDao<any>