https://github.com/golergka/pg-tx
Transactions for node-postgres
https://github.com/golergka/pg-tx
nodejs postgresql subtle-bugs transactions
Last synced: 9 months ago
JSON representation
Transactions for node-postgres
- Host: GitHub
- URL: https://github.com/golergka/pg-tx
- Owner: golergka
- License: mit
- Created: 2021-04-20T23:28:31.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2021-06-28T11:18:56.000Z (over 4 years ago)
- Last Synced: 2025-04-16T09:41:54.689Z (9 months ago)
- Topics: nodejs, postgresql, subtle-bugs, transactions
- Language: TypeScript
- Homepage:
- Size: 645 KB
- Stars: 5
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
Awesome Lists containing this project
README
# pg-tx - Transactions for node-postgres
[](https://badge.fury.io/js/pg-tx) [](https://deepscan.io/dashboard#view=project&tid=14626&pid=17725&bid=413894)
This package implements transactions on top of [node-postgres](http://node-postgres.com), based on [this answer](https://stackoverflow.com/a/65588782/312725), but improved to remove a class of subtle bugs.
## Features
* No use-after-release bugs
* Automatic transactions-inside-transactions with savepoints
* Can be used with either `Pool` or `PoolClient`
## Usage
```Typescript
import tx from `pg-tx`
const pg = new Pool()
await tx(pg, async (db) => {
await db.query(`UPDATE accounts SET money = money - 50 WHERE name = 'bob'`)
await db.query(`UPDATE accounts SET money = money + 50 WHERE name = 'alice'`)
})
await tx(pg, async (db) => {
await db.query(`UPDATE accounts SET money = money - 50 WHERE name = 'bob'`)
await db.query(`UPDATE accounts SET money = money + 50 WHERE name = 'debbie'`)
// Any errors thrown inside the callback will terminate the transaction
throw new Error(`screw Debbie`)
})
// You can also use it with other packages that use Pool or PoolClient, like pgtyped
import { sql } from '@pgtyped/query'
const updateAccount = sql`
UPDATE accounts
SET money = momey + $delta
WHERE name = $name
`
await tx(pg, async(db) => {
await udpateAccount.run({ name: 'bob', delta: -50 })
await udpateAccount.run({ name: 'charlie', delta: 50 })
})
```
## Why use this package
Naive approach to pg transactions, featured in [this answer](https://stackoverflow.com/a/65588782/312725) and used in many projects looks like this:
```Typescript
// DO NOT USE THIS CODE
export default async function tx(
pg: Pool,
callback: (db: PoolClient) => Promise
): Promise {
const client = await pg.connect()
await client.query(`BEGIN`)
try {
const result = await callback(client)
await client.query(`COMMIT`)
return result
} catch (e) {
await client.query(`ROLLBACK`)
throw e
} finally {
client.release()
}
}
```
However, this approach contains a subtle bug, because the `client` it passes to the callback stays valid after transaction finishes (successfully or not), and can be unknowingly used. In essence, it's a variation of use-after-free bug, but with database clients instead of memory.
Here's a demonstration of code that can trigger this condition:
```Typescript
async function failsQuickly(db: PoolClient) {
await db.query(`This query has an error`)
}
async function executesSlowly(db: PoolClient) {
// Takes a couple of seconds to complete
await externalApiCall()
// This operation will be executed OUTSIDE of transaction block!
await db.query(`
UPDATE external_api_calls
SET amount = amount + 1
WHERE service = 'some_service'
`)
}
await tx(pg, async (db) => {
await Promise.all([
failsQuickly(db),
executesSlowly(db)
])
})
```
To prevent this, we use ProxyClient, which implements a disposable pattern. After the client has been released, any attempts to use it will throw an error.
## Development
To run tests, specify `POSTGRES_URL` in `.env` file like this:
```
POSTGRES_URL="postgres://postgres:password@127.0.0.1:5432/test-db"
```