Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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"
}
}
]
}
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
cache: yarn
- run: yarn install --frozen-lockfile
- run: yarn lint

build:
timeout-minutes: 15
needs: lint
Expand All @@ -38,7 +39,6 @@ jobs:
fail-fast: false
matrix:
node:
- '16'
- '18'
- '20'
- '22'
Expand Down Expand Up @@ -73,4 +73,6 @@ jobs:
node-version: ${{ matrix.node }}
cache: yarn
- run: yarn install --frozen-lockfile
- run: yarn test
- name: Run tests (skip pg-transaction under Node < 18)
run: |
yarn test
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@
"lint": "eslint --cache 'packages/**/*.{js,ts,tsx}'"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@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"
"ts-node": "^10.9.0",
"typescript": "^5.2.0"
},
"prettier": {
"semi": false,
Expand Down
25 changes: 25 additions & 0 deletions packages/pg-transaction/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "pg-transaction",
"version": "2.0.0",
"main": "dist/index.js",
"license": "MIT",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"pretest": "tsc",
"test": "mocha dist/**/*.test.js"
},
"dependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=18.0.0"
},
"devDependencies": {
"@types/mocha": "^10.0.10",
"@types/pg": "^8.10.9",
"mocha": "^10.8.2",
"pg": "^8.11.3"
}
}
74 changes: 74 additions & 0 deletions packages/pg-transaction/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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(...args: any[]): any {
// console.log('Executing query:', ...args);
// @ts-ignore
return super.query(...args)
}

async [Symbol.asyncDispose]() {
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)')
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')
})

it('automatically commits on success', async () => {
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')
})

it('automatically rolls back on error', async () => {
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'])
await client.query('SELECT * FROM test_table')
throw new Error('Simulated error') // This will trigger a rollback
})
},
{
name: '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')
})

it('can return nothing from the transaction with correct type', async () => {
await using client = await getClient()

const _: void = await transaction(client, async () => {
await client.query('INSERT INTO test_table (name) VALUES ($1)', ['test'])
})
})
})
42 changes: 42 additions & 0 deletions packages/pg-transaction/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Client } from 'pg'

async function doTransaction(client: Client) {
await client.query('BEGIN')

let shouldRollback = false
let disposed = false

return {
async [Symbol.asyncDispose]() {
if (disposed) return
disposed = true

if (shouldRollback) {
await client.query('ROLLBACK')
} else {
await client.query('COMMIT')
}
},

rollback() {
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)

try {
const result = await fn()
// If we get here, success - transaction will auto-commit
return result
} catch (error) {
// If error occurs, mark for rollback
txn.rollback()
throw error
}
}

export { transaction as transaction }
16 changes: 16 additions & 0 deletions packages/pg-transaction/tsconfig.eslint.json
Original file line number Diff line number Diff line change
@@ -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"]
}
16 changes: 16 additions & 0 deletions packages/pg-transaction/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2022",
"module": "commonjs",
"lib": ["es2022", "ESNext.Disposable"],
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test"]
}
Loading