@naturalcycles/db-lib 9.1.1 → 9.2.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,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.
@@ -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.0",
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
  /**
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.