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

Commit be7af35

Browse files
feat: CommonDB.createTransaction experimental API
1 parent 86aa4bd commit be7af35

File tree

11 files changed

+497
-346
lines changed

11 files changed

+497
-346
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"@naturalcycles/nodejs-lib": "^13"
1414
},
1515
"devDependencies": {
16-
"@naturalcycles/bench-lib": "^3",
16+
"@naturalcycles/bench-lib": "^4",
1717
"@naturalcycles/dev-lib": "^17",
1818
"@types/node": "^22",
1919
"@vitest/coverage-v8": "^3",

src/adapter/inmemory/inMemory.db.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,13 @@ export class InMemoryDB implements CommonDB {
281281
}
282282
}
283283

284+
async createTransaction(opt: CommonDBTransactionOptions = {}): Promise<DBTransaction> {
285+
return new InMemoryDBTransaction(this, {
286+
readOnly: false,
287+
...opt,
288+
})
289+
}
290+
284291
async incrementBatch(
285292
table: string,
286293
prop: string,

src/base.common.db.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ export class BaseCommonDB implements CommonDB {
9393
// there's no try/catch and rollback, as there's nothing to rollback
9494
}
9595

96+
async createTransaction(_opt?: CommonDBTransactionOptions): Promise<FakeDBTransaction> {
97+
return new FakeDBTransaction(this)
98+
}
99+
96100
async incrementBatch(
97101
_table: string,
98102
_prop: string,

src/common.db.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
CommonDBSaveOptions,
1313
CommonDBStreamOptions,
1414
CommonDBTransactionOptions,
15+
DBTransaction,
1516
DBTransactionFn,
1617
RunQueryResult,
1718
} from './db.model'
@@ -151,6 +152,13 @@ export interface CommonDB {
151152
*/
152153
runInTransaction: (fn: DBTransactionFn, opt?: CommonDBTransactionOptions) => Promise<void>
153154

155+
/**
156+
* Experimental API to support more manual transaction control.
157+
*
158+
* @experimental
159+
*/
160+
createTransaction: (opt?: CommonDBTransactionOptions) => Promise<DBTransaction>
161+
154162
/**
155163
* Increments a value of a property by a given amount.
156164
* This is a batch operation, so it allows to increment multiple rows at once.

src/commondao/common.dao.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,6 +1307,11 @@ export class CommonDao<BM extends BaseDBEntity, DBM extends BaseDBEntity = BM, I
13071307
await this.cfg.db.ping()
13081308
}
13091309

1310+
async createTransaction(opt?: CommonDBTransactionOptions): Promise<CommonDaoTransaction> {
1311+
const tx = await this.cfg.db.createTransaction(opt)
1312+
return new CommonDaoTransaction(tx, this.cfg.logger!)
1313+
}
1314+
13101315
async runInTransaction<T = void>(
13111316
fn: CommonDaoTransactionFn<T>,
13121317
opt?: CommonDBTransactionOptions,
@@ -1419,8 +1424,17 @@ export class CommonDaoTransaction {
14191424
private logger: CommonLogger,
14201425
) {}
14211426

1427+
/**
1428+
* Commits the underlying DBTransaction.
1429+
* May throw.
1430+
*/
1431+
async commit(): Promise<void> {
1432+
await this.tx.commit()
1433+
}
1434+
14221435
/**
14231436
* Perform a graceful rollback without throwing/re-throwing any error.
1437+
* Never throws.
14241438
*/
14251439
async rollback(): Promise<void> {
14261440
try {

src/db.model.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ export interface DBTransaction {
2727
saveBatch: CommonDB['saveBatch']
2828
deleteByIds: CommonDB['deleteByIds']
2929

30+
/**
31+
* Commit the transaction.
32+
* May throw.
33+
*/
34+
commit: () => Promise<void>
35+
3036
/**
3137
* Perform a graceful rollback.
3238
* It'll rollback the transaction and won't throw/re-throw any errors.

src/testing/dbTest.ts renamed to src/testing/commonDBTest.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,18 @@ export async function runCommonDBTest(
294294
}
295295

296296
if (support.transactions) {
297+
/**
298+
* Returns expected items in the DB after the preparation.
299+
*/
300+
async function prepare(): Promise<TestItemDBM[]> {
301+
// cleanup
302+
await db.deleteByQuery(queryAll())
303+
304+
const itemsToSave: TestItemDBM[] = [items[0]!, { ...items[2]!, k1: 'k1_mod' }]
305+
await db.saveBatch(TEST_TABLE, itemsToSave)
306+
return itemsToSave
307+
}
308+
297309
test('transaction happy path', async () => {
298310
// cleanup
299311
await db.deleteByQuery(queryAll())
@@ -313,7 +325,24 @@ export async function runCommonDBTest(
313325
expectMatch(expected, rows, quirks)
314326
})
315327

328+
test('createTransaction happy path', async () => {
329+
// cleanup
330+
await db.deleteByQuery(queryAll())
331+
332+
const tx = await db.createTransaction()
333+
await tx.saveBatch(TEST_TABLE, items)
334+
await tx.saveBatch(TEST_TABLE, [{ ...items[2]!, k1: 'k1_mod' }])
335+
await tx.deleteByIds(TEST_TABLE, [items[1]!.id])
336+
await tx.commit()
337+
338+
const { rows } = await db.runQuery(queryAll())
339+
const expected = [items[0], { ...items[2]!, k1: 'k1_mod' }]
340+
expectMatch(expected, rows, quirks)
341+
})
342+
316343
test('transaction rollback', async () => {
344+
const expected = await prepare()
345+
317346
let err: any
318347

319348
try {
@@ -329,7 +358,6 @@ export async function runCommonDBTest(
329358
expect(err).toBeDefined()
330359

331360
const { rows } = await db.runQuery(queryAll())
332-
const expected = [items[0], { ...items[2]!, k1: 'k1_mod' }]
333361
expectMatch(expected, rows, quirks)
334362
})
335363
}

src/testing/daoTest.ts renamed to src/testing/commonDaoTest.ts

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { CommonDaoLogLevel, DBQuery } from '..'
55
import type { CommonDB } from '../common.db'
66
import { CommonDao } from '../commondao/common.dao'
77
import type { TestItemBM } from '.'
8-
import type { CommonDBImplementationQuirks } from './dbTest'
8+
import type { CommonDBImplementationQuirks } from './commonDBTest'
99
import {
1010
createTestItemBM,
1111
createTestItemsBM,
@@ -308,6 +308,18 @@ export async function runCommonDaoTest(
308308
}
309309

310310
if (support.transactions) {
311+
/**
312+
* Returns expected items in the DB after the preparation.
313+
*/
314+
async function prepare(): Promise<TestItemBM[]> {
315+
// cleanup
316+
await dao.query().deleteByQuery()
317+
318+
const itemsToSave: TestItemBM[] = [items[0]!, { ...items[2]!, k1: 'k1_mod' }]
319+
await dao.saveBatch(itemsToSave)
320+
return itemsToSave
321+
}
322+
311323
test('transaction happy path', async () => {
312324
// cleanup
313325
await dao.query().deleteByQuery()
@@ -345,16 +357,81 @@ export async function runCommonDaoTest(
345357
expectMatch(expected, rows, quirks)
346358
})
347359

360+
test('createTransaction happy path', async () => {
361+
// cleanup
362+
await dao.query().deleteByQuery()
363+
364+
// Test that id, created, updated are created
365+
const now = localTime.nowUnix()
366+
367+
const row = _omit(item1, ['id', 'created', 'updated'])
368+
let tx = await dao.createTransaction()
369+
await tx.save(dao, row)
370+
await tx.commit()
371+
372+
const loaded = await dao.query().runQuery()
373+
expect(loaded.length).toBe(1)
374+
expect(loaded[0]!.id).toBeDefined()
375+
expect(loaded[0]!.created).toBeGreaterThanOrEqual(now)
376+
expect(loaded[0]!.updated).toBe(loaded[0]!.created)
377+
378+
tx = await dao.createTransaction()
379+
await tx.deleteById(dao, loaded[0]!.id)
380+
await tx.commit()
381+
382+
// saveBatch [item1, 2, 3]
383+
// save item3 with k1: k1_mod
384+
// delete item2
385+
// remaining: item1, item3_with_k1_mod
386+
tx = await dao.createTransaction()
387+
await tx.saveBatch(dao, items)
388+
await tx.save(dao, { ...items[2]!, k1: 'k1_mod' })
389+
await tx.deleteById(dao, items[1]!.id)
390+
await tx.commit()
391+
392+
const rows = await dao.query().runQuery()
393+
const expected = [items[0], { ...items[2]!, k1: 'k1_mod' }]
394+
expectMatch(expected, rows, quirks)
395+
})
396+
348397
test('transaction rollback', async () => {
349-
await expect(
350-
dao.runInTransaction(async tx => {
398+
const expected = await prepare()
399+
400+
let err: any
401+
402+
try {
403+
await dao.runInTransaction(async tx => {
351404
await tx.deleteById(dao, items[2]!.id)
352405
await tx.save(dao, { ...items[0]!, k1: 5 as any }) // it should fail here
353-
}),
354-
).rejects.toThrow()
406+
})
407+
} catch (err_) {
408+
err = err_
409+
}
410+
411+
expect(err).toBeDefined()
412+
expect(err).toBeInstanceOf(Error)
413+
414+
const rows = await dao.query().runQuery()
415+
expectMatch(expected, rows, quirks)
416+
})
417+
418+
test('createTransaction rollback', async () => {
419+
const expected = await prepare()
420+
421+
let err: any
422+
try {
423+
const tx = await dao.createTransaction()
424+
await tx.deleteById(dao, items[2]!.id)
425+
await tx.save(dao, { ...items[0]!, k1: 5 as any }) // it should fail here
426+
await tx.commit()
427+
} catch (err_) {
428+
err = err_
429+
}
430+
431+
expect(err).toBeDefined()
432+
expect(err).toBeInstanceOf(Error)
355433

356434
const rows = await dao.query().runQuery()
357-
const expected = [items[0], { ...items[2]!, k1: 'k1_mod' }]
358435
expectMatch(expected, rows, quirks)
359436
})
360437

src/testing/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { runCommonDaoTest } from './daoTest'
2-
import type { CommonDBImplementationQuirks } from './dbTest'
3-
import { runCommonDBTest } from './dbTest'
1+
import { runCommonDaoTest } from './commonDaoTest'
2+
import type { CommonDBImplementationQuirks } from './commonDBTest'
3+
import { runCommonDBTest } from './commonDBTest'
44
import { runCommonKeyValueDaoTest } from './keyValueDaoTest'
55
import { runCommonKeyValueDBTest } from './keyValueDBTest'
66
import type { TestItemBM, TestItemDBM, TestItemTM } from './test.model'

src/transaction/dbTransaction.util.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export class FakeDBTransaction implements DBTransaction {
9898
constructor(protected db: CommonDB) {}
9999

100100
// no-op
101+
async commit(): Promise<void> {}
101102
async rollback(): Promise<void> {}
102103

103104
async getByIds<ROW extends ObjectWithId>(

0 commit comments

Comments
 (0)