Skip to content

Commit 6f7ac5f

Browse files
committed
Add pg-transaction module
1 parent 27a2754 commit 6f7ac5f

7 files changed

Lines changed: 863 additions & 38 deletions

File tree

packages/pg-transaction/.eslintrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"ignorePatterns": [
3+
"/dist/*{js,ts,map}",
4+
"/src",
5+
"/esm"
6+
]
7+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "pg-transaction",
3+
"version": "2.0.0",
4+
"main": "dist/index.js",
5+
"license": "MIT",
6+
"scripts": {
7+
"build": "tsc",
8+
"start": "node dist/index.js",
9+
"pretest": "tsc",
10+
"test": "mocha dist/**/*.test.js"
11+
},
12+
"dependencies": {
13+
"@types/node": "^24.0.14",
14+
"typescript": "^5.8.3"
15+
},
16+
"engines": {
17+
"node": ">=18.0.0"
18+
},
19+
"devDependencies": {
20+
"@types/mocha": "^10.0.10",
21+
"@types/pg": "^8.10.9",
22+
"mocha": "^10.8.2",
23+
"pg": "^8.11.3"
24+
}
25+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { strict as assert } from 'assert';
2+
import { Client } from 'pg';
3+
import { transaction } from '.';
4+
5+
class DisposableClient extends Client {
6+
// overwrite the query method and log the arguments and then dispatch to the original method
7+
override query(queryText: string, values?: any[]): Promise<any>;
8+
override query(queryConfig: any): Promise<any>;
9+
override query(queryStream: any): any;
10+
override query(...args: any[]): any {
11+
// console.log('Executing query:', ...args);
12+
// @ts-ignore
13+
return super.query(...args);
14+
}
15+
16+
async [Symbol.asyncDispose]() {
17+
await this.end();
18+
}
19+
}
20+
21+
async function getClient(): Promise<DisposableClient> {
22+
const client = new DisposableClient()
23+
await client.connect();
24+
await client.query('CREATE TEMP TABLE test_table (id SERIAL PRIMARY KEY, name TEXT)');
25+
return client
26+
}
27+
28+
29+
describe('transaction', () => {
30+
it('should create a client with an empty temp table', async () => {
31+
await using client = await getClient();
32+
const { rowCount } = await client.query('SELECT * FROM test_table');
33+
assert.equal(rowCount, 0, 'Temp table should be empty on creation');
34+
});
35+
36+
it('automatically commits on success', async () => {
37+
await using client = await getClient();
38+
39+
const result = await transaction(client, async () => {
40+
await client.query('INSERT INTO test_table (name) VALUES ($1)', ['test']);
41+
const { rows } = await client.query('SELECT * FROM test_table');
42+
return rows[0].name; // Should return 'test'
43+
});
44+
45+
assert.equal(result, 'test');
46+
});
47+
48+
it('automatically rolls back on error', async () => {
49+
await using client = await getClient();
50+
51+
// Assert that the transaction function rejects with the expected error
52+
await assert.rejects(
53+
async () => {
54+
await transaction(client, async () => {
55+
await client.query('INSERT INTO test_table (name) VALUES ($1)', ['test']);
56+
const { rows } = await client.query('SELECT * FROM test_table');
57+
throw new Error('Simulated error'); // This will trigger a rollback
58+
});
59+
},
60+
{
61+
name: 'Error',
62+
message: 'Simulated error'
63+
}
64+
);
65+
66+
// Verify that the transaction rolled back
67+
const { rowCount } = await client.query('SELECT * FROM test_table');
68+
assert.equal(rowCount, 0, 'Table should be empty after rollback');
69+
});
70+
71+
it('can return nothing from the transaction with correct type', async () => {
72+
await using client = await getClient();
73+
74+
const _nothing: void = await transaction(client, async () => {
75+
await client.query('INSERT INTO test_table (name) VALUES ($1)', ['test']);
76+
});
77+
});
78+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Client } from 'pg';
2+
3+
async function doTransaction(client: Client) {
4+
await client.query('BEGIN');
5+
6+
let shouldRollback = false;
7+
let disposed = false;
8+
9+
return {
10+
async [Symbol.asyncDispose]() {
11+
if (disposed) return;
12+
disposed = true;
13+
14+
if (shouldRollback) {
15+
await client.query('ROLLBACK');
16+
} else {
17+
await client.query('COMMIT');
18+
}
19+
},
20+
21+
rollback() {
22+
shouldRollback = true;
23+
}
24+
};
25+
}
26+
27+
// Auto-rollback wrapper that catches errors automatically
28+
async function transaction<T>(client: Client, fn: () => Promise<T>): Promise<T> {
29+
await using txn = await doTransaction(client);
30+
31+
try {
32+
const result = await fn();
33+
// If we get here, success - transaction will auto-commit
34+
return result;
35+
} catch (error) {
36+
// If error occurs, mark for rollback
37+
txn.rollback();
38+
throw error;
39+
}
40+
}
41+
42+
export { transaction as transaction };
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2022",
4+
"module": "commonjs",
5+
"lib": ["es2022", "ESNext.Disposable"],
6+
"outDir": "./dist",
7+
"rootDir": "./src",
8+
"declaration": true,
9+
"esModuleInterop": true,
10+
"forceConsistentCasingInFileNames": true,
11+
"strict": true,
12+
"skipLibCheck": true
13+
},
14+
"include": ["src/**/*"],
15+
"exclude": ["node_modules", "dist", "test"]
16+
}

0 commit comments

Comments
 (0)