This commit is contained in:
Brian Carlson 2025-07-17 22:21:46 -05:00
parent 922c28a13f
commit b7f9806d3d
7 changed files with 137 additions and 118 deletions

View File

@ -4,7 +4,7 @@
"extends": ["eslint:recommended", "plugin:prettier/recommended", "prettier"],
"ignorePatterns": ["node_modules", "coverage", "packages/pg-protocol/dist/**/*", "packages/pg-query-stream/dist/**/*"],
"parserOptions": {
"ecmaVersion": 2017,
"ecmaVersion": 2022,
"sourceType": "module"
},
"env": {
@ -30,6 +30,15 @@
"rules": {
"no-undef": "off"
}
},
{
"files": ["packages/pg-transaction/src/**/*.ts"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module",
"project": "./packages/pg-transaction/tsconfig.eslint.json"
}
}
]
}

View File

@ -21,14 +21,14 @@
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^6.17.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.1.2",
"lerna": "^3.19.0",
"prettier": "3.0.3",
"typescript": "^4.0.3"
"typescript": "^5.2.0"
},
"prettier": {
"semi": false,

View File

@ -1,7 +0,0 @@
{
"ignorePatterns": [
"/dist/*{js,ts,map}",
"/src",
"/esm"
]
}

View File

@ -1,78 +1,74 @@
import { strict as assert } from 'assert';
import { Client } from 'pg';
import { transaction } from '.';
import { strict as assert } from 'assert'
import { Client } from 'pg'
import { transaction } from '.'
class DisposableClient extends Client {
// overwrite the query method and log the arguments and then dispatch to the original method
override query(queryText: string, values?: any[]): Promise<any>;
override query(queryConfig: any): Promise<any>;
override query(queryStream: any): any;
override query(...args: any[]): any {
// console.log('Executing query:', ...args);
// @ts-ignore
return super.query(...args);
return super.query(...args)
}
async [Symbol.asyncDispose]() {
await this.end();
await this.end()
}
}
async function getClient(): Promise<DisposableClient> {
const client = new DisposableClient()
await client.connect();
await client.query('CREATE TEMP TABLE test_table (id SERIAL PRIMARY KEY, name TEXT)');
await client.connect()
await client.query('CREATE TEMP TABLE test_table (id SERIAL PRIMARY KEY, name TEXT)')
return client
}
describe('transaction', () => {
it('should create a client with an empty temp table', async () => {
await using client = await getClient();
const { rowCount } = await client.query('SELECT * FROM test_table');
assert.equal(rowCount, 0, 'Temp table should be empty on creation');
});
await using client = await getClient()
const { rowCount } = await client.query('SELECT * FROM test_table')
assert.equal(rowCount, 0, 'Temp table should be empty on creation')
})
it('automatically commits on success', async () => {
await using client = await getClient();
await using client = await getClient()
const result = await transaction(client, async () => {
await client.query('INSERT INTO test_table (name) VALUES ($1)', ['test']);
const { rows } = await client.query('SELECT * FROM test_table');
return rows[0].name; // Should return 'test'
});
assert.equal(result, 'test');
});
await client.query('INSERT INTO test_table (name) VALUES ($1)', ['test'])
const { rows } = await client.query('SELECT * FROM test_table')
return rows[0].name // Should return 'test'
})
assert.equal(result, 'test')
})
it('automatically rolls back on error', async () => {
await using client = await getClient();
await using client = await getClient()
// Assert that the transaction function rejects with the expected error
await assert.rejects(
async () => {
await transaction(client, async () => {
await client.query('INSERT INTO test_table (name) VALUES ($1)', ['test']);
const { rows } = await client.query('SELECT * FROM test_table');
throw new Error('Simulated error'); // This will trigger a rollback
});
await client.query('INSERT INTO test_table (name) VALUES ($1)', ['test'])
await client.query('SELECT * FROM test_table')
throw new Error('Simulated error') // This will trigger a rollback
})
},
{
name: 'Error',
message: 'Simulated error'
message: 'Simulated error',
}
);
)
// Verify that the transaction rolled back
const { rowCount } = await client.query('SELECT * FROM test_table');
assert.equal(rowCount, 0, 'Table should be empty after rollback');
});
const { rowCount } = await client.query('SELECT * FROM test_table')
assert.equal(rowCount, 0, 'Table should be empty after rollback')
})
it('can return nothing from the transaction with correct type', async () => {
await using client = await getClient();
const _nothing: void = await transaction(client, async () => {
await client.query('INSERT INTO test_table (name) VALUES ($1)', ['test']);
});
});
});
await using client = await getClient()
const _: void = await transaction(client, async () => {
await client.query('INSERT INTO test_table (name) VALUES ($1)', ['test'])
})
})
})

View File

@ -1,42 +1,42 @@
import { Client } from 'pg';
import { Client } from 'pg'
async function doTransaction(client: Client) {
await client.query('BEGIN');
let shouldRollback = false;
let disposed = false;
await client.query('BEGIN')
let shouldRollback = false
let disposed = false
return {
async [Symbol.asyncDispose]() {
if (disposed) return;
disposed = true;
if (disposed) return
disposed = true
if (shouldRollback) {
await client.query('ROLLBACK');
await client.query('ROLLBACK')
} else {
await client.query('COMMIT');
await client.query('COMMIT')
}
},
rollback() {
shouldRollback = true;
}
};
shouldRollback = true
},
}
}
// Auto-rollback wrapper that catches errors automatically
async function transaction<T>(client: Client, fn: () => Promise<T>): Promise<T> {
await using txn = await doTransaction(client);
await using txn = await doTransaction(client)
try {
const result = await fn();
const result = await fn()
// If we get here, success - transaction will auto-commit
return result;
return result
} catch (error) {
// If error occurs, mark for rollback
txn.rollback();
throw error;
txn.rollback()
throw error
}
}
export { transaction as transaction };
export { transaction as transaction }

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2022",
"module": "commonjs",
"lib": ["es2022"],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test"]
}

View File

@ -2348,25 +2348,17 @@
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/parser@^6.17.0":
version "6.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.17.0.tgz#8cd7a0599888ca6056082225b2fdf9a635bf32a1"
integrity sha512-C4bBaX2orvhK+LlwrY8oWGmSl4WolCfYm513gEccdWZj0CwGadbIADb0FtVEcI+WzUyjyoBj2JRP8g25E6IB8A==
"@typescript-eslint/parser@^7.0.0":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.18.0.tgz#83928d0f1b7f4afa974098c64b5ce6f9051f96a0"
integrity sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==
dependencies:
"@typescript-eslint/scope-manager" "6.17.0"
"@typescript-eslint/types" "6.17.0"
"@typescript-eslint/typescript-estree" "6.17.0"
"@typescript-eslint/visitor-keys" "6.17.0"
"@typescript-eslint/scope-manager" "7.18.0"
"@typescript-eslint/types" "7.18.0"
"@typescript-eslint/typescript-estree" "7.18.0"
"@typescript-eslint/visitor-keys" "7.18.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@6.17.0":
version "6.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.17.0.tgz#70e6c1334d0d76562dfa61aed9009c140a7601b4"
integrity sha512-RX7a8lwgOi7am0k17NUO0+ZmMOX4PpjLtLRgLmT1d3lBYdWH4ssBUbwdmc5pdRX8rXon8v9x8vaoOSpkHfcXGA==
dependencies:
"@typescript-eslint/types" "6.17.0"
"@typescript-eslint/visitor-keys" "6.17.0"
"@typescript-eslint/scope-manager@7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.0.0.tgz#15ea9abad2b56fc8f5c0b516775f41c86c5c8685"
@ -2375,6 +2367,14 @@
"@typescript-eslint/types" "7.0.0"
"@typescript-eslint/visitor-keys" "7.0.0"
"@typescript-eslint/scope-manager@7.18.0":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz#c928e7a9fc2c0b3ed92ab3112c614d6bd9951c83"
integrity sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==
dependencies:
"@typescript-eslint/types" "7.18.0"
"@typescript-eslint/visitor-keys" "7.18.0"
"@typescript-eslint/type-utils@7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.0.0.tgz#a4c7ae114414e09dbbd3c823b5924793f7483252"
@ -2385,29 +2385,15 @@
debug "^4.3.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/types@6.17.0":
version "6.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.17.0.tgz#844a92eb7c527110bf9a7d177e3f22bd5a2f40cb"
integrity sha512-qRKs9tvc3a4RBcL/9PXtKSehI/q8wuU9xYJxe97WFxnzH8NWWtcW3ffNS+EWg8uPvIerhjsEZ+rHtDqOCiH57A==
"@typescript-eslint/types@7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.0.0.tgz#2e5889c7fe3c873fc6dc6420aa77775f17cd5dc6"
integrity sha512-9ZIJDqagK1TTs4W9IyeB2sH/s1fFhN9958ycW8NRTg1vXGzzH5PQNzq6KbsbVGMT+oyyfa17DfchHDidcmf5cg==
"@typescript-eslint/typescript-estree@6.17.0":
version "6.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.17.0.tgz#b913d19886c52d8dc3db856903a36c6c64fd62aa"
integrity sha512-gVQe+SLdNPfjlJn5VNGhlOhrXz4cajwFd5kAgWtZ9dCZf4XJf8xmgCTLIqec7aha3JwgLI2CK6GY1043FRxZwg==
dependencies:
"@typescript-eslint/types" "6.17.0"
"@typescript-eslint/visitor-keys" "6.17.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
minimatch "9.0.3"
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/types@7.18.0":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.18.0.tgz#b90a57ccdea71797ffffa0321e744f379ec838c9"
integrity sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==
"@typescript-eslint/typescript-estree@7.0.0":
version "7.0.0"
@ -2423,6 +2409,20 @@
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/typescript-estree@7.18.0":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz#b5868d486c51ce8f312309ba79bdb9f331b37931"
integrity sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==
dependencies:
"@typescript-eslint/types" "7.18.0"
"@typescript-eslint/visitor-keys" "7.18.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
minimatch "^9.0.4"
semver "^7.6.0"
ts-api-utils "^1.3.0"
"@typescript-eslint/utils@7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.0.0.tgz#e43710af746c6ae08484f7afc68abc0212782c7e"
@ -2436,14 +2436,6 @@
"@typescript-eslint/typescript-estree" "7.0.0"
semver "^7.5.4"
"@typescript-eslint/visitor-keys@6.17.0":
version "6.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.17.0.tgz#3ed043709c39b43ec1e58694f329e0b0430c26b6"
integrity sha512-H6VwB/k3IuIeQOyYczyyKN8wH6ed8EwliaYHLxOIhyF0dYEIsN8+Bk3GE19qafeMKyZJJHP8+O1HiFhFLUNKSg==
dependencies:
"@typescript-eslint/types" "6.17.0"
eslint-visitor-keys "^3.4.1"
"@typescript-eslint/visitor-keys@7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.0.tgz#83cdadd193ee735fe9ea541f6a2b4d76dfe62081"
@ -2452,6 +2444,14 @@
"@typescript-eslint/types" "7.0.0"
eslint-visitor-keys "^3.4.1"
"@typescript-eslint/visitor-keys@7.18.0":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz#0564629b6124d67607378d0f0332a0495b25e7d7"
integrity sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==
dependencies:
"@typescript-eslint/types" "7.18.0"
eslint-visitor-keys "^3.4.3"
"@ungap/structured-clone@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
@ -8507,7 +8507,7 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3, semver@^7.7.1, semver@^7.7.2:
semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3, semver@^7.7.1, semver@^7.7.2:
version "7.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
@ -9384,6 +9384,11 @@ ts-api-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331"
integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==
ts-api-utils@^1.3.0:
version "1.4.3"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064"
integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==
ts-node@^8.5.4:
version "8.10.2"
resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz"
@ -9488,7 +9493,7 @@ typescript@^4.0.3:
resolved "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz"
integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
typescript@^5.8.3:
typescript@^5.2.0, typescript@^5.8.3:
version "5.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==