I've been using the excellent pg-promise library for a few years now, but my primary irk with it is that the stack traces are sometimes unhelpful. For example, the following test will fail (currently on nodejs v16.x):
const pgdb = pgp(config) // see pg-promise link for details, but just know that various db call methods are exposed as below:
it('should print a stack trace that includes all calling functions from a db call', async () => {
async function one() {
await two()
}
async function two() {
await three()
}
async function three() {
await pgdb.result('SELECT * FROM does_not_exist')
}
try {
await one()
assert.fail(`call to function 'one' should have thrown an error`)
} catch (err) {
const stack = err.stack
assert.include(stack, 'three', `could not find call to function 'three' in stack`)
assert.include(stack, 'two', `could not find call to function 'two' in stack`)
assert.include(stack, 'one', `could not find call to function 'one' in stack`)
}
)
This is because the stack comes out like this:
error: relation "does_not_exist" does not exist
at Parser.parseErrorMessage (/app/node_modules/pg-protocol/dist/parser.js:287:98)
at Parser.handlePacket (/app/node_modules/pg-protocol/dist/parser.js:126:29)
at Parser.parse (/app/node_modules/pg-protocol/dist/parser.js:39:38)
at Socket.<anonymous> (/app/node_modules/pg-protocol/dist/index.js:11:42)
at Socket.emit (node:events:390:28)
at addChunk (node:internal/streams/readable:315:12)
at readableAddChunk (node:internal/streams/readable:289:9)
at Socket.Readable.push (node:internal/streams/readable:228:10)
at TCP.onStreamRead (node:internal/stream_base_commons:199:23)
at TCP.callbackTrampoline (node:internal/async_hooks:130:17)
The stack trace has no context for where this was actually thrown. To fix this problem the author recommends using Bluebird promises for the better stack-trace capabilities, but recent advancements in node.js have rendered async/await better for performance.
I understand why this is happening, but it doesn't make my job easier. So, I thought I could us a Proxy to wrap the root database client object to get a proper stack trace:
// initializer module to do all the database client setup
const intermediate = pgp(_config) // create an intermediate client
// create a new object; creating a proxy from `intermediate` will throw
// `property 'result' is a read-only and non-configurable data property` errors
// when accessed through a proxy
const _pgdb = { ...intermediate }
const handler = {
get: function (target, prop) {
if (prop in target) {
const property = target[prop]
if (typeof property === 'function') {
return async function (...args) {
try {
return await property.apply(target, args)
} catch (err) {
throw new Error(`error calling database function '${prop}': ${err.message}`)
}
}
}
return property
}
// there are some properties like `$config` that aren't destructured from `intermediate`
if (prop in intermediate) {
return intermediate[prop]
}
return undefined
}
}
const pgdb = new Proxy(_pgdb, handler)
Stack traces now look like:
Error: error calling database function 'result': relation "does_not_exist" does not exist
at Proxy.<anonymous> (/app/db/initializer.js:109:19)
at runMicrotasks (<anonymous>)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async three (/app/test/dbal/utils.test.js:252:7)
at async two (/app/test/dbal/utils.test.js:249:7)
at async one (/app/test/dbal/utils.test.js:245:7)
at async Context.<anonymous> (/app/test/dbal/utils.test.js:256:7)
While this works and gives me the exact lines where the failure was thrown, it seems like a dirty hack and I feel like I'm missing something that could make this wrapper a little more elegant.