Skip to content

Commit 8b6fa0c

Browse files
committed
Add explicit mode
1 parent 0e8af9a commit 8b6fa0c

File tree

11 files changed

+164
-61
lines changed

11 files changed

+164
-61
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
# CHANGELOG
22

3-
# 0.1.0
3+
# 0.3.0
44

5-
- Initial commit
5+
- Add explicit mode
66

77
# 0.2.0
88

99
- Add handling of exceptions inside transactions
10+
11+
# 0.1.0
12+
13+
- Initial commit

README.md

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,18 @@ $ npm install pg pgtx
2929

3030
To use pgtx, wrap your PostgreSQL client with `pgtx`. You can then perform transactions as usual, and pgtx will manage savepoints automatically.
3131

32+
`pgtx` has two modes:
33+
34+
1. **Automated mode** runs inside nested functions and commits after the function ends. To roll back call `tx.rollback()` or throw an error.
35+
2. **Explicit mode** requires the user to call `tx.commit()` or `tx.rollback()` explicitly.
36+
37+
### Usage -- Automated mode
38+
39+
The caller provides an async function to the transaction method and committing happens after completion of this method.
40+
3241
```typescript
33-
import pgtx, { PgtxClient } from "pgtx";
34-
import { Client } from "pg";
42+
import pgtx, { PgtxClient } from 'pgtx';
43+
import { Client } from 'pg';
3544

3645
const client = new Client({
3746
/* connection config */
@@ -40,7 +49,7 @@ const db: PgtxClient = pgtx(client);
4049

4150
await db.transaction(async (tx) => {
4251
// Your transactional code here, e.g.
43-
await db.query("INSERT INTO ...", [value]);
52+
await db.query('INSERT INTO ...', [value]);
4453

4554
await trx.transaction(async (nestedTx) => {
4655
// Nested transaction handled with savepoint
@@ -56,7 +65,7 @@ await db.transaction(async (tx) => {
5665
// parent transactions can handle the exception as well
5766
try {
5867
await tx.transation(async (nestedTx) => {
59-
throw new Error("Some issue occured");
68+
throw new Error('Some issue occured');
6069
});
6170
} catch (e) {
6271
// Handle recovery scenario
@@ -66,13 +75,35 @@ await db.transaction(async (tx) => {
6675
});
6776
```
6877

78+
### Usage -- Explicit mode
79+
80+
The caller is responsible for calling either `commit()` or `rollback`. Be sure to handle failure scenario's using `try`/`catch`.
81+
82+
```typescript
83+
const client = new Client({
84+
/* connection config */
85+
});
86+
const db: PgtxClient = pgtx(client);
87+
88+
const tx = await db.transaction();
89+
// Other code here, e.g.:
90+
await tx.query('INSERT INTO ...', [value]);
91+
92+
const txNested = await tx.transaction(); // note the use of `tx` here
93+
// Other code here, e.g.:
94+
await txNested.query('INSERT INTO ...', [value]);
95+
await txNested.rollback();
96+
97+
await tx.commit();
98+
```
99+
69100
## API
70101

71102
**`PgtxClient`** extends `pg.Client` and adds:
72103

73-
- `async transaction(tx: PgtxClient => void)`: Begins a new transaction or savepoint if already in a transaction.
74-
- `async rollback()`: Rolls back the current transaction or savepoint.
75-
- `async commit()`: Commits the current transaction or releases the savepoint -- although typically you will not use this method
104+
- `transaction(txFn?: (tx: PgtxClient) => void): Promise<PgtxClient>`: Begins a new transaction or savepoint if already in a transaction. If `txFn` is not defined it sets the client to the child context.
105+
- `rollback(): Promise<PgtxClient>`: Rolls back the current transaction or savepoint. Returns the client in the parent context.
106+
- `commit(): Promise<PgtxClient>`: Commits the current transaction or releases the savepoint -- although typically you will not use this method. Returns the client in the parent context.
76107

77108
## Contributions
78109

eslint.config.mjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// @ts-check
22

3-
import eslint from "@eslint/js";
4-
import tseslint from "typescript-eslint";
5-
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
3+
import eslint from '@eslint/js';
4+
import tseslint from 'typescript-eslint';
5+
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
66

77
export default tseslint.config(
88
{
9-
ignores: ["dist/*"],
9+
ignores: ['dist/*'],
1010
},
1111
eslint.configs.recommended,
1212
...tseslint.configs.recommended,

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pgtx",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "Simple node-postgres wrapper that abstracts transactions and savepoints",
55
"repository": {
66
"type": "git",
@@ -51,4 +51,4 @@
5151
"peerDependencies": {
5252
"pg": "^8.12.0"
5353
}
54-
}
54+
}

prettier.config.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
trailingComma: 'all',
3+
tabWidth: 2,
4+
semi: true,
5+
singleQuote: true,
6+
};

src/index.test.ts

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
import { describe, test, expect } from "vitest";
2-
import { getClient, createTable, checkRows, insertRow } from "./test/util";
1+
import { describe, test, expect } from 'vitest';
2+
import { getClient, createTable, checkRows, insertRow } from './test/util';
33

4-
describe("happy flows", async () => {
5-
test("single level non explicit commit", async () => {
4+
describe('happy flows', async () => {
5+
test('single level non explicit commit', async () => {
66
const client = await getClient();
77
await client.transaction(async (tx) => {
8-
await createTable(tx, "table1");
9-
await insertRow(tx, "table1", 1);
8+
await createTable(tx, 'table1');
9+
await insertRow(tx, 'table1', 1);
1010
});
1111

12-
await checkRows(client, "table1", 1);
12+
await checkRows(client, 'table1', 1);
1313
});
1414

15-
test("nested multiple non explicit commits", async () => {
15+
test('nested multiple non explicit commits', async () => {
1616
const client = await getClient();
17-
const tableName = "table2";
17+
const tableName = 'table2';
1818

1919
await client.transaction(async (tx) => {
2020
await createTable(tx, tableName);
@@ -35,8 +35,8 @@ describe("happy flows", async () => {
3535
await checkRows(client, tableName, 3);
3636
});
3737

38-
test("simple rollback", async () => {
39-
const tableName = "table3";
38+
test('simple rollback', async () => {
39+
const tableName = 'table3';
4040
const client = await getClient();
4141
await createTable(client, tableName);
4242

@@ -48,8 +48,8 @@ describe("happy flows", async () => {
4848
await checkRows(client, tableName, 0);
4949
});
5050

51-
test("nested rollback", async () => {
52-
const tableName = "table4";
51+
test('nested rollback', async () => {
52+
const tableName = 'table4';
5353
const client = await getClient();
5454

5555
await client.transaction(async (tx) => {
@@ -64,8 +64,8 @@ describe("happy flows", async () => {
6464
await checkRows(client, tableName, 0);
6565
});
6666

67-
test("nested rollback with continuation", async () => {
68-
const tableName = "table5";
67+
test('nested rollback with continuation', async () => {
68+
const tableName = 'table5';
6969
const client = await getClient();
7070

7171
await client.transaction(async (tx) => {
@@ -85,9 +85,9 @@ describe("happy flows", async () => {
8585
});
8686
});
8787

88-
describe("exception handling", async () => {
89-
test("exception executes rollback", async () => {
90-
const tableName = "table6";
88+
describe('exception handling', async () => {
89+
test('exception executes rollback', async () => {
90+
const tableName = 'table6';
9191
const client = await getClient();
9292

9393
await createTable(client, tableName);
@@ -96,15 +96,15 @@ describe("exception handling", async () => {
9696
await insertRow(tx, tableName, 1);
9797
throw new Error();
9898
});
99-
} catch (e) {
99+
} catch {
100100
// catch-all
101101
}
102102

103103
await checkRows(client, tableName, 0);
104104
});
105105

106-
test("nested exception without try/catch rolls back everything", async () => {
107-
const tableName = "table7";
106+
test('nested exception without try/catch rolls back everything', async () => {
107+
const tableName = 'table7';
108108
const client = await getClient();
109109

110110
try {
@@ -113,7 +113,7 @@ describe("exception handling", async () => {
113113
await insertRow(tx, tableName, 1);
114114
await tx.transaction(async (tx2) => {
115115
await insertRow(tx2, tableName, 2);
116-
throw new Error("dummy-msg");
116+
throw new Error('dummy-msg');
117117
});
118118
});
119119
} catch (e) {
@@ -123,8 +123,8 @@ describe("exception handling", async () => {
123123
await checkRows(client, tableName, 0);
124124
});
125125

126-
test("nested exception executes rollback to savepoint", async () => {
127-
const tableName = "table8";
126+
test('nested exception executes rollback to savepoint', async () => {
127+
const tableName = 'table8';
128128
const client = await getClient();
129129

130130
await createTable(client, tableName);
@@ -135,11 +135,65 @@ describe("exception handling", async () => {
135135
await insertRow(tx2, tableName, 2);
136136
throw new Error();
137137
});
138-
} catch (e) {
138+
} catch {
139139
// catch error
140140
}
141141
});
142142

143143
await checkRows(client, tableName, 1);
144144
});
145145
});
146+
147+
describe('explicit mode', async () => {
148+
test('unnested transaction - commit', async () => {
149+
const tableName = 'table9';
150+
const client = await getClient();
151+
152+
await createTable(client, tableName);
153+
const tx = await client.transaction();
154+
await insertRow(tx, tableName, 1);
155+
await tx.commit();
156+
157+
await checkRows(client, tableName, 1);
158+
});
159+
160+
test('unnested transaction - rollback', async () => {
161+
const tableName = 'table10';
162+
const client = await getClient();
163+
164+
await createTable(client, tableName);
165+
const tx = await client.transaction();
166+
await insertRow(tx, tableName, 1);
167+
await tx.rollback();
168+
169+
await checkRows(client, tableName, 0);
170+
});
171+
172+
test('nested transaction - commit', async () => {
173+
const tableName = 'table11';
174+
const client = await getClient();
175+
176+
await createTable(client, tableName);
177+
const tx = await client.transaction();
178+
await tx.transaction(async (tx2) => {
179+
await insertRow(tx2, tableName, 1);
180+
});
181+
await tx.commit();
182+
183+
await checkRows(client, tableName, 1);
184+
});
185+
186+
test('nested transaction - rollback', async () => {
187+
const tableName = 'table11';
188+
const client = await getClient();
189+
190+
await createTable(client, tableName);
191+
const tx = await client.transaction();
192+
await tx.transaction(async (tx2) => {
193+
await insertRow(tx2, tableName, 1);
194+
});
195+
await tx.rollback();
196+
197+
await checkRows(client, tableName, 0);
198+
});
199+
});

src/index.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Client } from "pg";
2-
import { randomUUID } from "crypto";
1+
import { Client } from 'pg';
2+
import { randomUUID } from 'crypto';
33

44
enum TransactionStatus {
55
ACTIVE,
@@ -16,24 +16,30 @@ class TransactionContext {
1616
) {}
1717

1818
async transaction(
19-
childTransaction: (client: PgtxClient) => Promise<void>,
20-
): Promise<void> {
19+
childTransaction?: (client: PgtxClient) => Promise<void>,
20+
): Promise<PgtxClient> {
2121
let activeSavePointName: string | undefined = undefined;
2222

2323
if (this.parentTransaction) {
24-
activeSavePointName = "sp_" + randomUUID().replaceAll("-", "_");
24+
activeSavePointName = 'sp_' + randomUUID().replaceAll('-', '_');
2525
await this.client.query(`SAVEPOINT ${activeSavePointName}`);
2626
} else {
27-
await this.client.query("BEGIN");
27+
await this.client.query('BEGIN');
2828
}
2929

3030
const childCtx = new TransactionContext(
3131
this.client,
3232
this,
3333
activeSavePointName,
3434
);
35+
36+
const proxy = createProxy(this.client, childCtx);
37+
if (!childTransaction) {
38+
return proxy;
39+
}
40+
3541
try {
36-
await childTransaction(createProxy(this.client, childCtx));
42+
await childTransaction(proxy);
3743
} catch (e) {
3844
await childCtx.rollback();
3945
throw e;
@@ -42,33 +48,35 @@ class TransactionContext {
4248
if (childCtx.status === TransactionStatus.ACTIVE) {
4349
await childCtx.commit();
4450
}
51+
52+
return createProxy(this.client, this);
4553
}
4654

4755
async commit(): Promise<PgtxClient> {
4856
if (this.status !== TransactionStatus.ACTIVE) {
49-
throw new Error("Cannot commit a transaction that is not active");
57+
throw new Error('Cannot commit a transaction that is not active');
5058
}
5159
if (this.parentTransaction && this.activeSavePoint) {
5260
await this.client.query(`RELEASE SAVEPOINT ${this.activeSavePoint}`);
5361
return createProxy(this.client, this.parentTransaction);
5462
}
5563

56-
await this.client.query("COMMIT");
64+
await this.client.query('COMMIT');
5765
this.status = TransactionStatus.COMMITTED;
5866
return createProxy(this.client, this.parentTransaction || this);
5967
}
6068

6169
async rollback(): Promise<PgtxClient> {
6270
if (this.status !== TransactionStatus.ACTIVE) {
63-
throw new Error("Cannot roll back a transaction that is not active");
71+
throw new Error('Cannot roll back a transaction that is not active');
6472
}
6573
if (this.activeSavePoint && this.parentTransaction) {
6674
await this.client.query(`ROLLBACK TO SAVEPOINT ${this.activeSavePoint}`);
6775
this.status = TransactionStatus.CANCELLED;
6876
return createProxy(this.client, this.parentTransaction);
6977
}
7078

71-
await this.client.query("ROLLBACK");
79+
await this.client.query('ROLLBACK');
7280
this.status = TransactionStatus.CANCELLED;
7381
return createProxy(this.client, this.parentTransaction || this);
7482
}

0 commit comments

Comments
 (0)