Skip to content

Commit 3b5d829

Browse files
author
Toni Leigh Sharpe
authored
feature(automatic-transactions): Creates a decorator to make transactions easier for the developer (#76)
1 parent 1c78d07 commit 3b5d829

7 files changed

+365
-3
lines changed

README.md

+27
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,33 @@ fastify.listen(3000, err => {
230230
})
231231
```
232232

233+
### Transact route option
234+
It's possible to automatically wrap a route handler in a transaction by using the `transact` option when registering a route with Fastify. Note that the option must be scoped within a `pg` options object to take effect.
235+
236+
`query` commands can then be accessed at `request.pg` or `request.pg[name]` and `transact` can be set for either the root pg client with value `true` or for a pg client at a particular namespace with value `name`. Note that the namespace needs to be set when registering the plugin in order to be available on the request object.
237+
238+
```js
239+
// transact set for the route pg client
240+
fastify.get('/user/:id', { pg: { transact: true } }, (req, reply) => {
241+
// transaction wrapped queries, NO error handling
242+
req.pg.query('SELECT username FROM users WHERE id=1')
243+
req.pg.query('SELECT username FROM users WHERE id=2')
244+
req.pg.query('SELECT username FROM users WHERE id=3')
245+
})
246+
247+
// transact set for a pg client at name
248+
fastify.get('/user/:id', { pg: { transact: 'foo' } }, (req, reply) => {
249+
// transaction wrapped queries, NO error handling
250+
req.pg.foo.query('SELECT username FROM users WHERE id=1')
251+
req.pg.foo.query('SELECT username FROM users WHERE id=2')
252+
req.pg.foo.query('SELECT username FROM users WHERE id=3')
253+
})
254+
```
255+
256+
Important: rolling back a transaction relies on the handler failing and being caught by an `onError` hook. This means that the transaction wrapped route handler must not catch any errors internally.
257+
258+
In the plugin this works by using the `preHandler` hook to open the transaction, then the `onError` and `onSend` hooks to commit or rollback and release the client back to the pool.
259+
233260
## TypeScript Usage
234261

235262
Install the compiler and typings for pg module:

index.js

+80
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
const defaultPg = require('pg')
44
const fp = require('fastify-plugin')
55

6+
const addHandler = require('./lib/add-handler.js')
7+
8+
const transactionFailedSymbol = Symbol('transactionFailed')
9+
610
function transactionUtil (pool, fn, cb) {
711
pool.connect((err, client, done) => {
812
if (err) return cb(err)
@@ -52,6 +56,18 @@ function transact (fn, cb) {
5256
})
5357
}
5458

59+
function extractRequestClient (req, transact) {
60+
if (typeof transact !== 'string') {
61+
return req.pg
62+
}
63+
64+
const requestClient = req.pg[transact]
65+
if (!requestClient) {
66+
throw new Error(`request client '${transact}' does not exist`)
67+
}
68+
return requestClient
69+
}
70+
5571
function fastifyPostgres (fastify, options, next) {
5672
let pg = defaultPg
5773

@@ -102,6 +118,70 @@ function fastifyPostgres (fastify, options, next) {
102118
}
103119
}
104120

121+
if (!fastify.hasRequestDecorator('pg')) {
122+
fastify.decorateRequest('pg', null)
123+
}
124+
125+
fastify.addHook('onRoute', routeOptions => {
126+
const transact = routeOptions && routeOptions.pg && routeOptions.pg.transact
127+
128+
if (!transact) {
129+
return
130+
}
131+
if (typeof transact === 'string' && transact !== name) {
132+
return
133+
}
134+
if (name && transact === true) {
135+
return
136+
}
137+
138+
const preHandler = async (req, reply) => {
139+
const client = await pool.connect()
140+
141+
if (name) {
142+
if (!req.pg) {
143+
req.pg = {}
144+
}
145+
146+
if (client[name]) {
147+
throw new Error(`pg client '${name}' is a reserved keyword`)
148+
} else if (req.pg[name]) {
149+
throw new Error(`request client '${name}' has already been registered`)
150+
}
151+
152+
req.pg[name] = client
153+
} else {
154+
if (req.pg) {
155+
throw new Error('request client has already been registered')
156+
} else {
157+
req.pg = client
158+
}
159+
}
160+
161+
client.query('BEGIN')
162+
}
163+
164+
const onError = (req, reply, error, done) => {
165+
req[transactionFailedSymbol] = true
166+
extractRequestClient(req, transact).query('ROLLBACK', done)
167+
}
168+
169+
const onSend = async (req) => {
170+
const requestClient = extractRequestClient(req, transact)
171+
try {
172+
if (!req[transactionFailedSymbol]) {
173+
await requestClient.query('COMMIT')
174+
}
175+
} finally {
176+
requestClient.release()
177+
}
178+
}
179+
180+
routeOptions.preHandler = addHandler(routeOptions.preHandler, preHandler)
181+
routeOptions.onError = addHandler(routeOptions.onError, onError)
182+
routeOptions.onSend = addHandler(routeOptions.onSend, onSend)
183+
})
184+
105185
next()
106186
}
107187

lib/add-handler.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict'
2+
3+
module.exports = function addHandler (existingHandler, newHandler) {
4+
if (Array.isArray(existingHandler)) {
5+
return [
6+
...existingHandler,
7+
newHandler
8+
]
9+
} else if (typeof existingHandler === 'function') {
10+
return [existingHandler, newHandler]
11+
} else {
12+
return [newHandler]
13+
}
14+
}

test/add-handler.test.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict'
2+
3+
const t = require('tap')
4+
const test = t.test
5+
const addHandler = require('../lib/add-handler')
6+
7+
test('addHandler - ', t => {
8+
test('when existing handler is not defined', t => {
9+
t.plan(1)
10+
11+
const handlers = addHandler(
12+
undefined,
13+
'test'
14+
)
15+
16+
t.same(handlers, ['test'])
17+
})
18+
test('when existing handler is a array', t => {
19+
t.plan(1)
20+
21+
const handlers = addHandler(
22+
['test'],
23+
'again'
24+
)
25+
26+
t.same(handlers, ['test', 'again'])
27+
})
28+
test('when existing handler is a function', t => {
29+
t.plan(2)
30+
31+
const stub = () => 'test'
32+
33+
const handlers = addHandler(
34+
stub,
35+
'again'
36+
)
37+
38+
t.same(handlers[0](), 'test')
39+
t.same(handlers[1], 'again')
40+
})
41+
42+
t.end()
43+
})

test/initialization.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ test('Should throw when trying to register multiple instances without giving a n
110110
})
111111
})
112112

113-
test('Should not throw when registering a named instance and an unnamed instance)', (t) => {
113+
test('Should not throw when registering a named instance and an unnamed instance', (t) => {
114114
t.plan(1)
115115

116116
const fastify = Fastify()
@@ -191,7 +191,7 @@ test('fastify.pg namespace should exist', (t) => {
191191
})
192192
})
193193

194-
test('fastify.pg.test namespace should exist', (t) => {
194+
test('fastify.pg custom namespace should exist if a name is set', (t) => {
195195
t.plan(6)
196196

197197
const fastify = Fastify()

test/query.test.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const t = require('tap')
44
const test = t.test
55
const Fastify = require('fastify')
66
const fastifyPostgres = require('../index')
7+
78
const {
89
BAD_DB_NAME,
910
connectionString,
@@ -134,7 +135,7 @@ test('When fastify.pg root namespace is used:', (t) => {
134135
t.end()
135136
})
136137

137-
test('When fastify.pg.test namespace is used:', (t) => {
138+
test('When fastify.pg custom namespace is used:', (t) => {
138139
t.test('Should be able to connect and perform a query', (t) => {
139140
t.plan(4)
140141

0 commit comments

Comments
 (0)