@naturalcycles/db-lib 9.1.1 → 9.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { JsonSchemaObject, StringMap, JsonSchemaRootObject, ObjectWithId, CommonLogger } from '@naturalcycles/js-lib';
2
2
  import { ReadableTyped } from '@naturalcycles/nodejs-lib';
3
- import { CommonDB, CommonDBType, DBOperation, DBPatch, DBTransactionFn } from '../..';
3
+ import { CommonDB, CommonDBTransactionOptions, CommonDBType, DBOperation, DBPatch, DBTransactionFn } from '../..';
4
4
  import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions, DBTransaction, RunQueryResult } from '../../db.model';
5
5
  import { DBQuery } from '../../query/dbQuery';
6
6
  export interface InMemoryDBCfg {
@@ -12,6 +12,16 @@ export interface InMemoryDBCfg {
12
12
  * Reset cache respects this prefix (won't touch other namespaces!)
13
13
  */
14
14
  tablesPrefix: string;
15
+ /**
16
+ * Many DB implementations (e.g Datastore and Firestore) forbid doing
17
+ * read operations after a write/delete operation was done inside a Transaction.
18
+ *
19
+ * To help spot that type of bug - InMemoryDB by default has this setting to `true`,
20
+ * which will throw on such occasions.
21
+ *
22
+ * Defaults to true.
23
+ */
24
+ forbidTransactionReadAfterWrite?: boolean;
15
25
  /**
16
26
  * @default false
17
27
  *
@@ -78,7 +88,7 @@ export declare class InMemoryDB implements CommonDB {
78
88
  runQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<RunQueryResult<ROW>>;
79
89
  runQueryCount<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): Promise<number>;
80
90
  streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, _opt?: CommonDBOptions): ReadableTyped<ROW>;
81
- runInTransaction(fn: DBTransactionFn): Promise<void>;
91
+ runInTransaction(fn: DBTransactionFn, opt?: CommonDBTransactionOptions): Promise<void>;
82
92
  /**
83
93
  * Flushes all tables (all namespaces) at once.
84
94
  */
@@ -90,8 +100,10 @@ export declare class InMemoryDB implements CommonDB {
90
100
  }
91
101
  export declare class InMemoryDBTransaction implements DBTransaction {
92
102
  private db;
93
- constructor(db: InMemoryDB);
103
+ private opt;
104
+ constructor(db: InMemoryDB, opt: Required<CommonDBTransactionOptions>);
94
105
  ops: DBOperation[];
106
+ writeOperationHappened: boolean;
95
107
  getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: CommonDBOptions): Promise<ROW[]>;
96
108
  saveBatch<ROW extends Partial<ObjectWithId>>(table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
97
109
  deleteByIds(table: string, ids: string[], opt?: CommonDBOptions): Promise<number>;
@@ -20,6 +20,7 @@ class InMemoryDB {
20
20
  this.cfg = {
21
21
  // defaults
22
22
  tablesPrefix: '',
23
+ forbidTransactionReadAfterWrite: true,
23
24
  persistenceEnabled: false,
24
25
  persistZip: true,
25
26
  persistentStoragePath: './tmp/inmemorydb',
@@ -146,8 +147,11 @@ class InMemoryDB {
146
147
  const table = this.cfg.tablesPrefix + q.table;
147
148
  return node_stream_1.Readable.from((0, __1.queryInMemory)(q, Object.values(this.data[table] || {})));
148
149
  }
149
- async runInTransaction(fn) {
150
- const tx = new InMemoryDBTransaction(this);
150
+ async runInTransaction(fn, opt = {}) {
151
+ const tx = new InMemoryDBTransaction(this, {
152
+ readOnly: false,
153
+ ...opt,
154
+ });
151
155
  try {
152
156
  await fn(tx);
153
157
  await tx.commit();
@@ -213,14 +217,22 @@ class InMemoryDB {
213
217
  }
214
218
  exports.InMemoryDB = InMemoryDB;
215
219
  class InMemoryDBTransaction {
216
- constructor(db) {
220
+ constructor(db, opt) {
217
221
  this.db = db;
222
+ this.opt = opt;
218
223
  this.ops = [];
224
+ // used to enforce forbidReadAfterWrite setting
225
+ this.writeOperationHappened = false;
219
226
  }
220
227
  async getByIds(table, ids, opt) {
228
+ if (this.db.cfg.forbidTransactionReadAfterWrite) {
229
+ (0, js_lib_1._assert)(!this.writeOperationHappened, `InMemoryDBTransaction: read operation attempted after write operation`);
230
+ }
221
231
  return await this.db.getByIds(table, ids, opt);
222
232
  }
223
233
  async saveBatch(table, rows, opt) {
234
+ (0, js_lib_1._assert)(!this.opt.readOnly, `InMemoryDBTransaction: saveBatch(${table}) called in readOnly mode`);
235
+ this.writeOperationHappened = true;
224
236
  this.ops.push({
225
237
  type: 'saveBatch',
226
238
  table,
@@ -229,6 +241,8 @@ class InMemoryDBTransaction {
229
241
  });
230
242
  }
231
243
  async deleteByIds(table, ids, opt) {
244
+ (0, js_lib_1._assert)(!this.opt.readOnly, `InMemoryDBTransaction: deleteByIds(${table}) called in readOnly mode`);
245
+ this.writeOperationHappened = true;
232
246
  this.ops.push({
233
247
  type: 'deleteByIds',
234
248
  table,
@@ -1,7 +1,7 @@
1
1
  import { JsonSchemaObject, JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib';
2
2
  import { ReadableTyped } from '@naturalcycles/nodejs-lib';
3
3
  import { CommonDB, CommonDBSupport, CommonDBType } from './common.db';
4
- import { CommonDBOptions, CommonDBSaveOptions, DBPatch, DBTransactionFn, RunQueryResult } from './db.model';
4
+ import { CommonDBOptions, CommonDBSaveOptions, CommonDBTransactionOptions, DBPatch, DBTransactionFn, RunQueryResult } from './db.model';
5
5
  import { DBQuery } from './query/dbQuery';
6
6
  /**
7
7
  * No-op implementation of CommonDB interface.
@@ -22,5 +22,5 @@ export declare class BaseCommonDB implements CommonDB {
22
22
  saveBatch<ROW extends Partial<ObjectWithId>>(table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
23
23
  streamQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>): ReadableTyped<ROW>;
24
24
  deleteByIds(table: string, ids: string[], opt?: CommonDBOptions): Promise<number>;
25
- runInTransaction(fn: DBTransactionFn): Promise<void>;
25
+ runInTransaction(fn: DBTransactionFn, opt?: CommonDBTransactionOptions): Promise<void>;
26
26
  }
@@ -49,7 +49,7 @@ class BaseCommonDB {
49
49
  async deleteByIds(table, ids, opt) {
50
50
  throw new Error('deleteByIds is not implemented');
51
51
  }
52
- async runInTransaction(fn) {
52
+ async runInTransaction(fn, opt) {
53
53
  const tx = new dbTransaction_util_1.FakeDBTransaction(this);
54
54
  await fn(tx);
55
55
  // there's no try/catch and rollback, as there's nothing to rollback
@@ -1,6 +1,6 @@
1
1
  import { JsonSchemaObject, JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib';
2
2
  import type { ReadableTyped } from '@naturalcycles/nodejs-lib';
3
- import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, DBPatch, DBTransactionFn, RunQueryResult } from './db.model';
3
+ import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions, CommonDBStreamOptions, CommonDBTransactionOptions, DBPatch, DBTransactionFn, RunQueryResult } from './db.model';
4
4
  import { DBQuery } from './query/dbQuery';
5
5
  export declare enum CommonDBType {
6
6
  'document' = "document",
@@ -104,8 +104,11 @@ export interface CommonDB {
104
104
  * Transaction is automatically committed if fn resolves normally.
105
105
  * Transaction is rolled back if fn throws, the error is re-thrown in that case.
106
106
  * Graceful rollback is allowed on tx.rollback()
107
+ *
108
+ * By default, transaction is read-write,
109
+ * unless specified as readOnly in CommonDBTransactionOptions.
107
110
  */
108
- runInTransaction: (fn: DBTransactionFn) => Promise<void>;
111
+ runInTransaction: (fn: DBTransactionFn, opt?: CommonDBTransactionOptions) => Promise<void>;
109
112
  }
110
113
  /**
111
114
  * Manifest of supported features.
@@ -2,7 +2,7 @@
2
2
  import { Transform } from 'node:stream';
3
3
  import { AnyObject, AsyncMapper, CommonLogger, JsonSchemaObject, JsonSchemaRootObject, ObjectWithId, Saved, UnixTimestampMillisNumber, Unsaved, ZodSchema } from '@naturalcycles/js-lib';
4
4
  import { AjvSchema, ObjectSchema, ReadableTyped } from '@naturalcycles/nodejs-lib';
5
- import { DBModelType, DBPatch, DBTransaction, RunQueryResult } from '../db.model';
5
+ import { CommonDBTransactionOptions, DBModelType, DBPatch, DBTransaction, RunQueryResult } from '../db.model';
6
6
  import { DBQuery, RunnableDBQuery } from '../query/dbQuery';
7
7
  import { CommonDaoCfg, CommonDaoCreateOptions, CommonDaoOptions, CommonDaoSaveBatchOptions, CommonDaoSaveOptions, CommonDaoStreamDeleteOptions, CommonDaoStreamForEachOptions, CommonDaoStreamOptions, CommonDaoStreamSaveOptions } from './common.dao.model';
8
8
  /**
@@ -169,7 +169,7 @@ export declare class CommonDao<BM extends Partial<ObjectWithId>, DBM extends Obj
169
169
  * Proxy to this.cfg.db.ping
170
170
  */
171
171
  ping(): Promise<void>;
172
- runInTransaction(fn: CommonDaoTransactionFn): Promise<void>;
172
+ runInTransaction(fn: CommonDaoTransactionFn, opt?: CommonDBTransactionOptions): Promise<void>;
173
173
  protected logResult(started: number, op: string, res: any, table: string): void;
174
174
  protected logSaveResult(started: number, op: string, table: string): void;
175
175
  protected logStarted(op: string, table: string, force?: boolean): UnixTimestampMillisNumber;
@@ -991,7 +991,7 @@ class CommonDao {
991
991
  async ping() {
992
992
  await this.cfg.db.ping();
993
993
  }
994
- async runInTransaction(fn) {
994
+ async runInTransaction(fn, opt) {
995
995
  await this.cfg.db.runInTransaction(async (tx) => {
996
996
  const daoTx = new CommonDaoTransaction(tx, this.cfg.logger);
997
997
  try {
@@ -1001,7 +1001,7 @@ class CommonDao {
1001
1001
  await daoTx.rollback();
1002
1002
  throw err;
1003
1003
  }
1004
- });
1004
+ }, opt);
1005
1005
  }
1006
1006
  logResult(started, op, res, table) {
1007
1007
  if (!this.cfg.logLevel)
@@ -29,6 +29,13 @@ export interface DBTransaction {
29
29
  */
30
30
  rollback: () => Promise<void>;
31
31
  }
32
+ export interface CommonDBTransactionOptions {
33
+ /**
34
+ * Default is false.
35
+ * If set to true - Transaction is created as read-only.
36
+ */
37
+ readOnly?: boolean;
38
+ }
32
39
  export interface CommonDBOptions {
33
40
  /**
34
41
  * If passed - the operation will be performed in the context of that DBTransaction.
package/package.json CHANGED
@@ -40,7 +40,7 @@
40
40
  "engines": {
41
41
  "node": ">=18.12"
42
42
  },
43
- "version": "9.1.1",
43
+ "version": "9.2.1",
44
44
  "description": "Lowest Common Denominator API to supported Databases",
45
45
  "keywords": [
46
46
  "db",
@@ -32,6 +32,7 @@ import {
32
32
  import {
33
33
  CommonDB,
34
34
  commonDBFullSupport,
35
+ CommonDBTransactionOptions,
35
36
  CommonDBType,
36
37
  DBIncrement,
37
38
  DBOperation,
@@ -58,6 +59,17 @@ export interface InMemoryDBCfg {
58
59
  */
59
60
  tablesPrefix: string
60
61
 
62
+ /**
63
+ * Many DB implementations (e.g Datastore and Firestore) forbid doing
64
+ * read operations after a write/delete operation was done inside a Transaction.
65
+ *
66
+ * To help spot that type of bug - InMemoryDB by default has this setting to `true`,
67
+ * which will throw on such occasions.
68
+ *
69
+ * Defaults to true.
70
+ */
71
+ forbidTransactionReadAfterWrite?: boolean
72
+
61
73
  /**
62
74
  * @default false
63
75
  *
@@ -96,6 +108,7 @@ export class InMemoryDB implements CommonDB {
96
108
  this.cfg = {
97
109
  // defaults
98
110
  tablesPrefix: '',
111
+ forbidTransactionReadAfterWrite: true,
99
112
  persistenceEnabled: false,
100
113
  persistZip: true,
101
114
  persistentStoragePath: './tmp/inmemorydb',
@@ -273,8 +286,12 @@ export class InMemoryDB implements CommonDB {
273
286
  return Readable.from(queryInMemory(q, Object.values(this.data[table] || {}) as ROW[]))
274
287
  }
275
288
 
276
- async runInTransaction(fn: DBTransactionFn): Promise<void> {
277
- const tx = new InMemoryDBTransaction(this)
289
+ async runInTransaction(fn: DBTransactionFn, opt: CommonDBTransactionOptions = {}): Promise<void> {
290
+ const tx = new InMemoryDBTransaction(this, {
291
+ readOnly: false,
292
+ ...opt,
293
+ })
294
+
278
295
  try {
279
296
  await fn(tx)
280
297
  await tx.commit()
@@ -361,15 +378,28 @@ export class InMemoryDB implements CommonDB {
361
378
  }
362
379
 
363
380
  export class InMemoryDBTransaction implements DBTransaction {
364
- constructor(private db: InMemoryDB) {}
381
+ constructor(
382
+ private db: InMemoryDB,
383
+ private opt: Required<CommonDBTransactionOptions>,
384
+ ) {}
365
385
 
366
386
  ops: DBOperation[] = []
367
387
 
388
+ // used to enforce forbidReadAfterWrite setting
389
+ writeOperationHappened = false
390
+
368
391
  async getByIds<ROW extends ObjectWithId>(
369
392
  table: string,
370
393
  ids: string[],
371
394
  opt?: CommonDBOptions,
372
395
  ): Promise<ROW[]> {
396
+ if (this.db.cfg.forbidTransactionReadAfterWrite) {
397
+ _assert(
398
+ !this.writeOperationHappened,
399
+ `InMemoryDBTransaction: read operation attempted after write operation`,
400
+ )
401
+ }
402
+
373
403
  return await this.db.getByIds(table, ids, opt)
374
404
  }
375
405
 
@@ -378,6 +408,13 @@ export class InMemoryDBTransaction implements DBTransaction {
378
408
  rows: ROW[],
379
409
  opt?: CommonDBSaveOptions<ROW>,
380
410
  ): Promise<void> {
411
+ _assert(
412
+ !this.opt.readOnly,
413
+ `InMemoryDBTransaction: saveBatch(${table}) called in readOnly mode`,
414
+ )
415
+
416
+ this.writeOperationHappened = true
417
+
381
418
  this.ops.push({
382
419
  type: 'saveBatch',
383
420
  table,
@@ -387,6 +424,13 @@ export class InMemoryDBTransaction implements DBTransaction {
387
424
  }
388
425
 
389
426
  async deleteByIds(table: string, ids: string[], opt?: CommonDBOptions): Promise<number> {
427
+ _assert(
428
+ !this.opt.readOnly,
429
+ `InMemoryDBTransaction: deleteByIds(${table}) called in readOnly mode`,
430
+ )
431
+
432
+ this.writeOperationHappened = true
433
+
390
434
  this.ops.push({
391
435
  type: 'deleteByIds',
392
436
  table,
@@ -4,6 +4,7 @@ import { CommonDB, CommonDBSupport, CommonDBType } from './common.db'
4
4
  import {
5
5
  CommonDBOptions,
6
6
  CommonDBSaveOptions,
7
+ CommonDBTransactionOptions,
7
8
  DBPatch,
8
9
  DBTransactionFn,
9
10
  RunQueryResult,
@@ -83,7 +84,7 @@ export class BaseCommonDB implements CommonDB {
83
84
  throw new Error('deleteByIds is not implemented')
84
85
  }
85
86
 
86
- async runInTransaction(fn: DBTransactionFn): Promise<void> {
87
+ async runInTransaction(fn: DBTransactionFn, opt?: CommonDBTransactionOptions): Promise<void> {
87
88
  const tx = new FakeDBTransaction(this)
88
89
  await fn(tx)
89
90
  // there's no try/catch and rollback, as there's nothing to rollback
package/src/common.db.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  CommonDBOptions,
6
6
  CommonDBSaveOptions,
7
7
  CommonDBStreamOptions,
8
+ CommonDBTransactionOptions,
8
9
  DBPatch,
9
10
  DBTransactionFn,
10
11
  RunQueryResult,
@@ -163,8 +164,11 @@ export interface CommonDB {
163
164
  * Transaction is automatically committed if fn resolves normally.
164
165
  * Transaction is rolled back if fn throws, the error is re-thrown in that case.
165
166
  * Graceful rollback is allowed on tx.rollback()
167
+ *
168
+ * By default, transaction is read-write,
169
+ * unless specified as readOnly in CommonDBTransactionOptions.
166
170
  */
167
- runInTransaction: (fn: DBTransactionFn) => Promise<void>
171
+ runInTransaction: (fn: DBTransactionFn, opt?: CommonDBTransactionOptions) => Promise<void>
168
172
  }
169
173
 
170
174
  /**
@@ -43,7 +43,13 @@ import {
43
43
  writableVoid,
44
44
  } from '@naturalcycles/nodejs-lib'
45
45
  import { DBLibError } from '../cnst'
46
- import { DBModelType, DBPatch, DBTransaction, RunQueryResult } from '../db.model'
46
+ import {
47
+ CommonDBTransactionOptions,
48
+ DBModelType,
49
+ DBPatch,
50
+ DBTransaction,
51
+ RunQueryResult,
52
+ } from '../db.model'
47
53
  import { DBQuery, RunnableDBQuery } from '../query/dbQuery'
48
54
  import {
49
55
  CommonDaoCfg,
@@ -1331,7 +1337,10 @@ export class CommonDao<
1331
1337
  await this.cfg.db.ping()
1332
1338
  }
1333
1339
 
1334
- async runInTransaction(fn: CommonDaoTransactionFn): Promise<void> {
1340
+ async runInTransaction(
1341
+ fn: CommonDaoTransactionFn,
1342
+ opt?: CommonDBTransactionOptions,
1343
+ ): Promise<void> {
1335
1344
  await this.cfg.db.runInTransaction(async tx => {
1336
1345
  const daoTx = new CommonDaoTransaction(tx, this.cfg.logger!)
1337
1346
 
@@ -1341,7 +1350,7 @@ export class CommonDao<
1341
1350
  await daoTx.rollback()
1342
1351
  throw err
1343
1352
  }
1344
- })
1353
+ }, opt)
1345
1354
  }
1346
1355
 
1347
1356
  protected logResult(started: number, op: string, res: any, table: string): void {
package/src/db.model.ts CHANGED
@@ -34,6 +34,14 @@ export interface DBTransaction {
34
34
  rollback: () => Promise<void>
35
35
  }
36
36
 
37
+ export interface CommonDBTransactionOptions {
38
+ /**
39
+ * Default is false.
40
+ * If set to true - Transaction is created as read-only.
41
+ */
42
+ readOnly?: boolean
43
+ }
44
+
37
45
  export interface CommonDBOptions {
38
46
  /**
39
47
  * If passed - the operation will be performed in the context of that DBTransaction.