@naturalcycles/db-lib 8.46.1 → 8.48.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.
package/src/common.db.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  CommonDBOptions,
6
6
  CommonDBSaveOptions,
7
7
  CommonDBStreamOptions,
8
+ DBPatch,
8
9
  RunQueryResult,
9
10
  } from './db.model'
10
11
  import { DBQuery } from './query/dbQuery'
@@ -42,17 +43,6 @@ export interface CommonDB {
42
43
  opt?: CommonDBCreateOptions,
43
44
  ): Promise<void>
44
45
 
45
- // GET
46
- /**
47
- * Order of items returned is not guaranteed to match order of ids.
48
- * (Such limitation exists because Datastore doesn't support it).
49
- */
50
- getByIds<ROW extends ObjectWithId>(
51
- table: string,
52
- ids: ROW['id'][],
53
- opt?: CommonDBOptions,
54
- ): Promise<ROW[]>
55
-
56
46
  // QUERY
57
47
  /**
58
48
  * Order by 'id' is not supported by all implementations (for example, Datastore doesn't support it).
@@ -84,14 +74,32 @@ export interface CommonDB {
84
74
  * Returns number of deleted items.
85
75
  * Not supported by all implementations (e.g Datastore will always return same number as number of ids).
86
76
  */
87
- deleteByIds<ROW extends ObjectWithId>(
88
- table: string,
89
- ids: ROW['id'][],
77
+ deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions): Promise<number>
78
+
79
+ /**
80
+ * Applies patch to the rows returned by the query.
81
+ *
82
+ * Example:
83
+ *
84
+ * UPDATE table SET A = B where $QUERY_CONDITION
85
+ *
86
+ * patch would be { A: 'B' } for that query.
87
+ *
88
+ * Supports "increment query", example:
89
+ *
90
+ * UPDATE table SET A = A + 1
91
+ *
92
+ * In that case patch would look like:
93
+ * { A: DBIncrement(1) }
94
+ *
95
+ * Returns number of rows affected.
96
+ */
97
+ updateByQuery<ROW extends ObjectWithId>(
98
+ q: DBQuery<ROW>,
99
+ patch: DBPatch<ROW>,
90
100
  opt?: CommonDBOptions,
91
101
  ): Promise<number>
92
102
 
93
- deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: CommonDBOptions): Promise<number>
94
-
95
103
  // TRANSACTION
96
104
  /**
97
105
  * Should be implemented as a Transaction (best effort), which means that
@@ -40,6 +40,7 @@ import {
40
40
  DBDeleteByIdsOperation,
41
41
  DBModelType,
42
42
  DBOperation,
43
+ DBPatch,
43
44
  DBSaveBatchOperation,
44
45
  RunQueryResult,
45
46
  } from '../db.model'
@@ -133,14 +134,17 @@ export class CommonDao<
133
134
 
134
135
  if (opt.timeout) {
135
136
  // todo: possibly remove it after debugging is done
136
- dbm = (
137
- await pTimeout(() => this.cfg.db.getByIds<DBM>(table, [id]), {
137
+ dbm = await pTimeout(
138
+ async () => {
139
+ return (await this.cfg.db.runQuery(DBQuery.create<DBM>(table).filterEq('id', id))).rows[0]
140
+ },
141
+ {
138
142
  timeout: opt.timeout,
139
143
  name: `getById(${table})`,
140
- })
141
- )[0]
144
+ },
145
+ )
142
146
  } else {
143
- dbm = (await this.cfg.db.getByIds<DBM>(table, [id]))[0]
147
+ dbm = (await this.cfg.db.runQuery(DBQuery.create<DBM>(table).filterEq('id', id))).rows[0]
144
148
  }
145
149
 
146
150
  const bm = opt.raw ? (dbm as any) : await this.dbmToBM(dbm, opt)
@@ -170,7 +174,7 @@ export class CommonDao<
170
174
  const op = `getByIdAsDBM(${id})`
171
175
  const table = opt.table || this.cfg.table
172
176
  const started = this.logStarted(op, table)
173
- let [dbm] = await this.cfg.db.getByIds<DBM>(table, [id])
177
+ let [dbm] = (await this.cfg.db.runQuery(DBQuery.create<DBM>(table).filterEq('id', id))).rows
174
178
  if (!opt.raw) {
175
179
  dbm = this.anyToDBM(dbm!, opt)
176
180
  }
@@ -185,7 +189,7 @@ export class CommonDao<
185
189
  const op = `getByIdAsTM(${id})`
186
190
  const table = opt.table || this.cfg.table
187
191
  const started = this.logStarted(op, table)
188
- const [dbm] = await this.cfg.db.getByIds<DBM>(table, [id])
192
+ const [dbm] = (await this.cfg.db.runQuery(DBQuery.create<DBM>(table).filterEq('id', id))).rows
189
193
  if (opt.raw) {
190
194
  this.logResult(started, op, dbm, table)
191
195
  return (dbm as any) || null
@@ -200,7 +204,7 @@ export class CommonDao<
200
204
  const op = `getByIds ${ids.length} id(s) (${_truncate(ids.slice(0, 10).join(', '), 50)})`
201
205
  const table = opt.table || this.cfg.table
202
206
  const started = this.logStarted(op, table)
203
- const dbms = await this.cfg.db.getByIds<DBM>(table, ids)
207
+ const dbms = (await this.cfg.db.runQuery(DBQuery.create<DBM>(table).filterIn('id', ids))).rows
204
208
  const bms = opt.raw ? (dbms as any) : await this.dbmsToBM(dbms, opt)
205
209
  this.logResult(started, op, bms, table)
206
210
  return bms
@@ -210,7 +214,7 @@ export class CommonDao<
210
214
  const op = `getByIdsAsDBM ${ids.length} id(s) (${_truncate(ids.slice(0, 10).join(', '), 50)})`
211
215
  const table = opt.table || this.cfg.table
212
216
  const started = this.logStarted(op, table)
213
- const dbms = await this.cfg.db.getByIds<DBM>(table, ids)
217
+ const dbms = (await this.cfg.db.runQuery(DBQuery.create<DBM>(table).filterIn('id', ids))).rows
214
218
  this.logResult(started, op, dbms, table)
215
219
  return dbms
216
220
  }
@@ -266,7 +270,8 @@ export class CommonDao<
266
270
 
267
271
  private async ensureUniqueId(table: string, dbm: DBM): Promise<void> {
268
272
  // todo: retry N times
269
- const existing = await this.cfg.db.getByIds<DBM>(table, [dbm.id])
273
+ const existing = (await this.cfg.db.runQuery(DBQuery.create<DBM>(table).filterEq('id', dbm.id)))
274
+ .rows
270
275
  if (existing.length) {
271
276
  throw new AppError(DBLibError.NON_UNIQUE_ID, {
272
277
  table,
@@ -868,9 +873,9 @@ export class CommonDao<
868
873
  const op = `deleteById(${id})`
869
874
  const table = opt.table || this.cfg.table
870
875
  const started = this.logStarted(op, table)
871
- const ids = await this.cfg.db.deleteByIds(table, [id])
876
+ const count = await this.cfg.db.deleteByQuery(DBQuery.create(table).filterEq('id', id))
872
877
  this.logSaveResult(started, op, table)
873
- return ids
878
+ return count
874
879
  }
875
880
 
876
881
  async deleteByIds(ids: ID[], opt: CommonDaoOptions = {}): Promise<number> {
@@ -879,9 +884,9 @@ export class CommonDao<
879
884
  const op = `deleteByIds(${ids.join(', ')})`
880
885
  const table = opt.table || this.cfg.table
881
886
  const started = this.logStarted(op, table)
882
- const deletedIds = await this.cfg.db.deleteByIds(table, ids)
887
+ const count = await this.cfg.db.deleteByQuery(DBQuery.create(table).filterIn('id', ids))
883
888
  this.logSaveResult(started, op, table)
884
- return deletedIds
889
+ return count
885
890
  }
886
891
 
887
892
  /**
@@ -911,7 +916,10 @@ export class CommonDao<
911
916
  transformBuffer<string>({ batchSize }),
912
917
  transformMap<string[], void>(
913
918
  async ids => {
914
- deleted += await this.cfg.db.deleteByIds(q.table, ids, opt)
919
+ deleted += await this.cfg.db.deleteByQuery(
920
+ DBQuery.create(q.table).filterIn('id', ids),
921
+ opt,
922
+ )
915
923
  },
916
924
  {
917
925
  predicate: _passthroughPredicate,
@@ -934,6 +942,29 @@ export class CommonDao<
934
942
  return deleted
935
943
  }
936
944
 
945
+ async updateById(id: ID, patch: DBPatch<DBM>, opt: CommonDaoOptions = {}): Promise<number> {
946
+ return await this.updateByQuery(this.query().filterEq('id', id), patch, opt)
947
+ }
948
+
949
+ async updateByIds(ids: ID[], patch: DBPatch<DBM>, opt: CommonDaoOptions = {}): Promise<number> {
950
+ return await this.updateByQuery(this.query().filterIn('id', ids), patch, opt)
951
+ }
952
+
953
+ async updateByQuery(
954
+ q: DBQuery<DBM>,
955
+ patch: DBPatch<DBM>,
956
+ opt: CommonDaoOptions = {},
957
+ ): Promise<number> {
958
+ this.requireWriteAccess()
959
+ this.requireObjectMutability(opt)
960
+ q.table = opt.table || q.table
961
+ const op = `updateByQuery(${q.pretty()})`
962
+ const started = this.logStarted(op, q.table)
963
+ const updated = await this.cfg.db.updateByQuery(q, patch, opt)
964
+ this.logSaveResult(started, op, q.table)
965
+ return updated
966
+ }
967
+
937
968
  // CONVERSIONS
938
969
 
939
970
  async dbmToBM(_dbm: undefined, opt?: CommonDaoOptions): Promise<undefined>
package/src/db.model.ts CHANGED
@@ -76,3 +76,22 @@ export enum DBModelType {
76
76
  BM = 'BM',
77
77
  TM = 'TM',
78
78
  }
79
+
80
+ /**
81
+ * Allows to construct a query similar to:
82
+ *
83
+ * UPDATE table SET A = A + 1
84
+ *
85
+ * In this case DBIncement.of(1) will be needed.
86
+ */
87
+ export class DBIncrement {
88
+ private constructor(public amount: number) {}
89
+
90
+ static of(amount: number): DBIncrement {
91
+ return new DBIncrement(amount)
92
+ }
93
+ }
94
+
95
+ export type DBPatch<ROW extends Partial<ObjectWithId>> = Partial<
96
+ Record<keyof ROW, ROW[keyof ROW] | DBIncrement>
97
+ >
@@ -5,9 +5,15 @@ import {
5
5
  _truncate,
6
6
  Saved,
7
7
  AnyObject,
8
+ _objectAssign,
8
9
  } from '@naturalcycles/js-lib'
9
10
  import { ReadableTyped } from '@naturalcycles/nodejs-lib'
10
- import { CommonDaoOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions } from '..'
11
+ import {
12
+ CommonDaoOptions,
13
+ CommonDaoStreamForEachOptions,
14
+ CommonDaoStreamOptions,
15
+ DBPatch,
16
+ } from '..'
11
17
  import { CommonDao } from '../commondao/common.dao'
12
18
  import { RunQueryResult } from '../db.model'
13
19
 
@@ -118,6 +124,11 @@ export class DBQuery<ROW extends ObjectWithId = AnyObjectWithId> {
118
124
  return this
119
125
  }
120
126
 
127
+ filterIn(name: keyof ROW, val: any[]): this {
128
+ this._filters.push({ name, op: 'in', val })
129
+ return this
130
+ }
131
+
121
132
  limit(limit: number): this {
122
133
  this._limitValue = limit
123
134
  return this
@@ -162,7 +173,7 @@ export class DBQuery<ROW extends ObjectWithId = AnyObjectWithId> {
162
173
  }
163
174
 
164
175
  clone(): DBQuery<ROW> {
165
- return Object.assign(new DBQuery<ROW>(this.table), {
176
+ return _objectAssign(new DBQuery<ROW>(this.table), {
166
177
  _filters: [...this._filters],
167
178
  _limitValue: this._limitValue,
168
179
  _offsetValue: this._offsetValue,
@@ -269,6 +280,10 @@ export class RunnableDBQuery<
269
280
  return await this.dao.runQueryCount(this, opt)
270
281
  }
271
282
 
283
+ async updateByQuery(patch: DBPatch<DBM>, opt?: CommonDaoOptions): Promise<number> {
284
+ return await this.dao.updateByQuery(this, patch, opt)
285
+ }
286
+
272
287
  async streamQueryForEach(
273
288
  mapper: AsyncMapper<Saved<BM>, void>,
274
289
  opt?: CommonDaoStreamForEachOptions<Saved<BM>>,
@@ -1,6 +1,6 @@
1
1
  import { pDelay, _deepCopy, _pick, _sortBy, _omit, localTime } from '@naturalcycles/js-lib'
2
2
  import { readableToArray, transformNoOp } from '@naturalcycles/nodejs-lib'
3
- import { CommonDaoLogLevel } from '..'
3
+ import { CommonDaoLogLevel, DBQuery } from '..'
4
4
  import { CommonDB } from '../common.db'
5
5
  import { CommonDao } from '../commondao/common.dao'
6
6
  import { CommonDBImplementationFeatures, CommonDBImplementationQuirks, expectMatch } from './dbTest'
@@ -75,9 +75,12 @@ export function runCommonDaoTest(
75
75
  // DELETE ALL initially
76
76
  test('deleteByIds test items', async () => {
77
77
  const rows = await dao.query().select(['id']).runQuery()
78
- await db.deleteByIds(
79
- TEST_TABLE,
80
- rows.map(i => i.id),
78
+ await db.deleteByQuery(
79
+ DBQuery.create(TEST_TABLE).filter(
80
+ 'id',
81
+ 'in',
82
+ rows.map(r => r.id),
83
+ ),
81
84
  )
82
85
  })
83
86
 
@@ -156,7 +159,11 @@ export function runCommonDaoTest(
156
159
  // GET not empty
157
160
  test('getByIds all items', async () => {
158
161
  const rows = await dao.getByIds(items.map(i => i.id).concat('abcd'))
159
- expectMatch(expectedItems, rows, quirks)
162
+ expectMatch(
163
+ expectedItems,
164
+ _sortBy(rows, r => r.id),
165
+ quirks,
166
+ )
160
167
  })
161
168
 
162
169
  // QUERY
@@ -258,11 +265,7 @@ export function runCommonDaoTest(
258
265
 
259
266
  test('cleanup', async () => {
260
267
  // CLEAN UP
261
- const rows = await dao.query().select(['id']).runQuery()
262
- await db.deleteByIds(
263
- TEST_TABLE,
264
- rows.map(i => i.id),
265
- )
268
+ await dao.query().deleteByQuery()
266
269
  })
267
270
  }
268
271
 
@@ -1,6 +1,7 @@
1
1
  import { pDelay, pMap, _filterObject, _pick, _sortBy } from '@naturalcycles/js-lib'
2
2
  import { readableToArray } from '@naturalcycles/nodejs-lib'
3
3
  import { CommonDB } from '../common.db'
4
+ import { DBIncrement, DBPatch } from '../db.model'
4
5
  import { DBQuery } from '../query/dbQuery'
5
6
  import { DBTransaction } from '../transaction/dbTransaction'
6
7
  import {
@@ -25,6 +26,10 @@ export interface CommonDBImplementationFeatures {
25
26
  insert?: boolean
26
27
  update?: boolean
27
28
 
29
+ updateByQuery?: boolean
30
+
31
+ dbIncrement?: boolean
32
+
28
33
  createTable?: boolean
29
34
  tableSchemas?: boolean
30
35
 
@@ -87,6 +92,8 @@ export function runCommonDBTest(
87
92
  dbQuerySelectFields = true,
88
93
  insert = true,
89
94
  update = true,
95
+ updateByQuery = true,
96
+ dbIncrement = true,
90
97
  streaming = true,
91
98
  strongConsistency = true,
92
99
  bufferSupport = true,
@@ -122,9 +129,12 @@ export function runCommonDBTest(
122
129
  // DELETE ALL initially
123
130
  test('deleteByIds test items', async () => {
124
131
  const { rows } = await db.runQuery(queryAll().select(['id']))
125
- await db.deleteByIds(
126
- TEST_TABLE,
127
- rows.map(i => i.id),
132
+ await db.runQuery(
133
+ queryAll().filter(
134
+ 'id',
135
+ 'in',
136
+ rows.map(i => i.id),
137
+ ),
128
138
  )
129
139
  })
130
140
 
@@ -138,17 +148,17 @@ export function runCommonDBTest(
138
148
 
139
149
  // GET empty
140
150
  test('getByIds(item1.id) should return empty', async () => {
141
- const [item1Loaded] = await db.getByIds<TestItemDBM>(TEST_TABLE, [item1.id])
151
+ const [item1Loaded] = (await db.runQuery(queryAll().filterEq('id', item1.id))).rows
142
152
  // console.log(a)
143
153
  expect(item1Loaded).toBeUndefined()
144
154
  })
145
155
 
146
156
  test('getByIds([]) should return []', async () => {
147
- expect(await db.getByIds(TEST_TABLE, [])).toEqual([])
157
+ expect((await db.runQuery(queryAll().filter('id', 'in', []))).rows).toEqual([])
148
158
  })
149
159
 
150
160
  test('getByIds(...) should return empty', async () => {
151
- expect(await db.getByIds(TEST_TABLE, ['abc', 'abcd'])).toEqual([])
161
+ expect((await db.runQuery(queryAll().filter('id', 'in', ['abc', 'abcd']))).rows).toEqual([])
152
162
  })
153
163
 
154
164
  // SAVE
@@ -160,7 +170,7 @@ export function runCommonDBTest(
160
170
  }
161
171
  deepFreeze(item3)
162
172
  await db.saveBatch(TEST_TABLE, [item3])
163
- const item3Loaded = (await db.getByIds<TestItemDBM>(TEST_TABLE, [item3.id]))[0]!
173
+ const item3Loaded = (await db.runQuery(queryAll().filterEq('id', item3.id))).rows[0]!
164
174
  expectMatch([item3], [item3Loaded], quirks)
165
175
  expect(item3Loaded.k2).toBeNull()
166
176
  })
@@ -177,7 +187,7 @@ export function runCommonDBTest(
177
187
  delete expected.k2
178
188
 
179
189
  await db.saveBatch(TEST_TABLE, [item3])
180
- const item3Loaded = (await db.getByIds<TestItemDBM>(TEST_TABLE, [item3.id]))[0]!
190
+ const item3Loaded = (await db.runQuery(queryAll().filterEq('id', item3.id))).rows[0]!
181
191
  expectMatch([expected], [item3Loaded], quirks)
182
192
  expect(item3Loaded.k2).toBeUndefined()
183
193
  expect(Object.keys(item3Loaded)).not.toContain('k2')
@@ -212,8 +222,14 @@ export function runCommonDBTest(
212
222
 
213
223
  // GET not empty
214
224
  test('getByIds all items', async () => {
215
- const rows = await db.getByIds<TestItemDBM>(TEST_TABLE, items.map(i => i.id).concat('abcd'))
216
- expectMatch(items, rows, quirks)
225
+ const rows = (
226
+ await db.runQuery(queryAll().filter('id', 'in', items.map(i => i.id).concat('abcd')))
227
+ ).rows
228
+ expectMatch(
229
+ items,
230
+ _sortBy(rows, r => r.id),
231
+ quirks,
232
+ )
217
233
  })
218
234
 
219
235
  // QUERY
@@ -333,8 +349,8 @@ export function runCommonDBTest(
333
349
  b1,
334
350
  }
335
351
  await db.saveBatch(TEST_TABLE, [item])
336
- const [loaded] = await db.getByIds<TestItemDBM>(TEST_TABLE, [item.id])
337
- const b1Loaded = loaded!.b1!
352
+ const loaded = (await db.runQuery(queryAll().filterEq('id', item.id))).rows[0]!
353
+ const b1Loaded = loaded.b1!
338
354
  // console.log({
339
355
  // b11: typeof b1,
340
356
  // b12: typeof b1Loaded,
@@ -383,14 +399,61 @@ export function runCommonDBTest(
383
399
  })
384
400
  }
385
401
 
402
+ if (updateByQuery) {
403
+ test('updateByQuery simple', async () => {
404
+ // cleanup, reset initial data
405
+ await db.deleteByQuery(queryAll())
406
+ await db.saveBatch(TEST_TABLE, items)
407
+
408
+ const patch: DBPatch<TestItemDBM> = {
409
+ k3: 5,
410
+ k2: 'abc',
411
+ }
412
+
413
+ await db.updateByQuery(DBQuery.create<TestItemDBM>(TEST_TABLE).filterEq('even', true), patch)
414
+
415
+ const { rows } = await db.runQuery(queryAll())
416
+ const expected = items.map(r => {
417
+ if (r.even) {
418
+ return { ...r, ...patch }
419
+ }
420
+ return r
421
+ })
422
+ expectMatch(expected, rows, quirks)
423
+ })
424
+
425
+ if (dbIncrement) {
426
+ test('updateByQuery DBIncrement', async () => {
427
+ // cleanup, reset initial data
428
+ await db.deleteByQuery(queryAll())
429
+ await db.saveBatch(TEST_TABLE, items)
430
+
431
+ const patch: DBPatch<TestItemDBM> = {
432
+ k3: DBIncrement.of(1),
433
+ k2: 'abcd',
434
+ }
435
+
436
+ await db.updateByQuery(
437
+ DBQuery.create<TestItemDBM>(TEST_TABLE).filterEq('even', true),
438
+ patch,
439
+ )
440
+
441
+ const { rows } = await db.runQuery(queryAll())
442
+ const expected = items.map(r => {
443
+ if (r.even) {
444
+ return { ...r, ...patch, k3: (r.k3 || 0) + 1 }
445
+ }
446
+ return r
447
+ })
448
+ expectMatch(expected, rows, quirks)
449
+ })
450
+ }
451
+ }
452
+
386
453
  if (querying) {
387
454
  test('cleanup', async () => {
388
455
  // CLEAN UP
389
- const { rows } = await db.runQuery(queryAll().select(['id']))
390
- await db.deleteByIds(
391
- TEST_TABLE,
392
- rows.map(i => i.id),
393
- )
456
+ await db.deleteByQuery(queryAll())
394
457
  })
395
458
  }
396
459
  }
@@ -1,5 +1,6 @@
1
1
  import type { CommonDB } from '../common.db'
2
2
  import { CommonDBSaveOptions, DBOperation } from '../db.model'
3
+ import { DBQuery } from '../query/dbQuery'
3
4
  import { DBTransaction } from './dbTransaction'
4
5
 
5
6
  /**
@@ -80,7 +81,10 @@ export async function commitDBTransactionSimple(
80
81
  if (op.type === 'saveBatch') {
81
82
  await db.saveBatch(op.table, op.rows, { ...op.opt, ...opt })
82
83
  } else if (op.type === 'deleteByIds') {
83
- await db.deleteByIds(op.table, op.ids, { ...op.opt, ...opt })
84
+ await db.deleteByQuery(DBQuery.create(op.table).filter('id', 'in', op.ids), {
85
+ ...op.opt,
86
+ ...opt,
87
+ })
84
88
  } else {
85
89
  throw new Error(`DBOperation not supported: ${(op as any).type}`)
86
90
  }