Skip to content
This repository was archived by the owner on May 25, 2025. It is now read-only.

Commit 2ec161a

Browse files
feat: CommonDBTransactionOptions, forbidTransactionReadAfterWrite
1 parent de67d7e commit 2ec161a

File tree

5 files changed

+63
-6
lines changed

5 files changed

+63
-6
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- { uses: actions/setup-node@v4, with: { node-version: 'lts/*', cache: 'yarn' } }
1515

1616
# Cache for npm/npx in ~/.npm
17-
- uses: actions/cache@v3
17+
- uses: actions/cache@v4
1818
with:
1919
path: ~/.npm
2020
key: npm-v1-${{ runner.os }}

src/adapter/inmemory/inMemory.db.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
import {
3333
CommonDB,
3434
commonDBFullSupport,
35+
CommonDBTransactionOptions,
3536
CommonDBType,
3637
DBIncrement,
3738
DBOperation,
@@ -58,6 +59,17 @@ export interface InMemoryDBCfg {
5859
*/
5960
tablesPrefix: string
6061

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+
6173
/**
6274
* @default false
6375
*
@@ -96,6 +108,7 @@ export class InMemoryDB implements CommonDB {
96108
this.cfg = {
97109
// defaults
98110
tablesPrefix: '',
111+
forbidTransactionReadAfterWrite: true,
99112
persistenceEnabled: false,
100113
persistZip: true,
101114
persistentStoragePath: './tmp/inmemorydb',
@@ -273,8 +286,12 @@ export class InMemoryDB implements CommonDB {
273286
return Readable.from(queryInMemory(q, Object.values(this.data[table] || {}) as ROW[]))
274287
}
275288

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+
278295
try {
279296
await fn(tx)
280297
await tx.commit()
@@ -361,15 +378,28 @@ export class InMemoryDB implements CommonDB {
361378
}
362379

363380
export class InMemoryDBTransaction implements DBTransaction {
364-
constructor(private db: InMemoryDB) {}
381+
constructor(
382+
private db: InMemoryDB,
383+
private opt: Required<CommonDBTransactionOptions>,
384+
) {}
365385

366386
ops: DBOperation[] = []
367387

388+
// used to enforce forbidReadAfterWrite setting
389+
writeOperationHappened = false
390+
368391
async getByIds<ROW extends ObjectWithId>(
369392
table: string,
370393
ids: string[],
371394
opt?: CommonDBOptions,
372395
): Promise<ROW[]> {
396+
if (this.db.cfg.forbidTransactionReadAfterWrite) {
397+
_assert(
398+
!this.writeOperationHappened,
399+
`InMemoryDBTransaction: read operation attempted after write operation`,
400+
)
401+
}
402+
373403
return await this.db.getByIds(table, ids, opt)
374404
}
375405

@@ -378,6 +408,13 @@ export class InMemoryDBTransaction implements DBTransaction {
378408
rows: ROW[],
379409
opt?: CommonDBSaveOptions<ROW>,
380410
): Promise<void> {
411+
_assert(
412+
!this.opt.readOnly,
413+
`InMemoryDBTransaction: saveBatch(${table}) called in readOnly mode`,
414+
)
415+
416+
this.writeOperationHappened = true
417+
381418
this.ops.push({
382419
type: 'saveBatch',
383420
table,
@@ -387,6 +424,13 @@ export class InMemoryDBTransaction implements DBTransaction {
387424
}
388425

389426
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+
390434
this.ops.push({
391435
type: 'deleteByIds',
392436
table,

src/base.common.db.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CommonDB, CommonDBSupport, CommonDBType } from './common.db'
44
import {
55
CommonDBOptions,
66
CommonDBSaveOptions,
7+
CommonDBTransactionOptions,
78
DBPatch,
89
DBTransactionFn,
910
RunQueryResult,
@@ -83,7 +84,7 @@ export class BaseCommonDB implements CommonDB {
8384
throw new Error('deleteByIds is not implemented')
8485
}
8586

86-
async runInTransaction(fn: DBTransactionFn): Promise<void> {
87+
async runInTransaction(fn: DBTransactionFn, opt?: CommonDBTransactionOptions): Promise<void> {
8788
const tx = new FakeDBTransaction(this)
8889
await fn(tx)
8990
// there's no try/catch and rollback, as there's nothing to rollback

src/common.db.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
CommonDBOptions,
66
CommonDBSaveOptions,
77
CommonDBStreamOptions,
8+
CommonDBTransactionOptions,
89
DBPatch,
910
DBTransactionFn,
1011
RunQueryResult,
@@ -163,8 +164,11 @@ export interface CommonDB {
163164
* Transaction is automatically committed if fn resolves normally.
164165
* Transaction is rolled back if fn throws, the error is re-thrown in that case.
165166
* Graceful rollback is allowed on tx.rollback()
167+
*
168+
* By default, transaction is read-write,
169+
* unless specified as readOnly in CommonDBTransactionOptions.
166170
*/
167-
runInTransaction: (fn: DBTransactionFn) => Promise<void>
171+
runInTransaction: (fn: DBTransactionFn, opt?: CommonDBTransactionOptions) => Promise<void>
168172
}
169173

170174
/**

src/db.model.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ export interface DBTransaction {
3434
rollback: () => Promise<void>
3535
}
3636

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+
3745
export interface CommonDBOptions {
3846
/**
3947
* If passed - the operation will be performed in the context of that DBTransaction.

0 commit comments

Comments
 (0)