pgpw
Opinionated wrapper around the pg-promise library.
This module provides the following features:
- connection pool singleton for the entire application;
- multiple database objects managed in one place;
- normalization to the native Promise;
- extended error handling with the use of verror;
- stubs API to be used in unit testing.
v5 Breaking Changes
Upgrade pg-promise
to v10, as v8 has stopped working in Node.js 14. No API breaking change has been introduced.
Archive documentation: v1, v2, v3, v4.
Installation
$ npm install pgpw --save
Usage
You should initialize database object(s) once per application (e.g., in your application's main function):
const pgpw = require('pgpw');
/**
* If your application works with two databases,
* you can initialize them both here
*/
// This database object won't provide any default retry strategy
pgpw.init('db_one', {
host: 'localhost',
port: 5432,
database: 'db_1',
user: 'app',
password: ''
});
// This database object will provide the given default retry
// strategy (which can be amended or cancelled by a particular execution)
pgpw.init(
'db_two',
{
host: process.env.DB2_HOST,
port: process.env.DB2_PORT,
database: process.env.DB2_NAME,
user: process.env.DB2_USER,
password: process.env.DB2_PSW
},
{
initialDelayMs: 200,
maxAttempts: 3
exponent: 2
}
);
Then in a module, you have access to initialized database objects by their names
// module.js
const db1 = require('pgpw')('db_one');
const db2 = require('pgpw')('db_two');
// Execute a function
db1.execFunc('my_func_1', [param1, param2])
.then(recordset => processReturnedRecordset(recordset))
.catch(err => handleErrorSomehow(err));
// Execute a function with retry options
db2.execFunc(
'my_func_1',
[param1, param2],
{
retry: {
initialDelayMs: 100,
maxAttempts: 5,
exponent: 2
}
}
)
.then(recordset => processReturnedRecordset(recordset))
.catch(err => handleErrorSomehow(err));
// Transaction example
db1.transaction(t => t.batch([
t.func('my_func_1', [param1, param2]),
t.func('my_func_2', [param3, param4])
]))
.then(([ recordset1, recordset2 ]) => {
processReturnedRecordset(recordset1);
processReturnedRecordset(recordset2);
})
.catch(err => handleErrorSomehow(err));
// Transaction with transaction mode settings and retry options
const transactionMode = {
isolationLevel: db.isolationLevel.serializable,
isReadOnly: true,
isDeferrable: true
};
const options = {
retry: {
initialDelayMs: 100,
maxAttempts: 5,
exponent: 2
}
}
db1.transaction(
transactionMode,
t => t.batch([
t.func('my_func_1', [param1, param2]),
t.func('my_func_2', [param3, param4])
]),
options
)
.then(([ recordset1, recordset2 ]) => {
processReturnedRecordset(recordset1);
processReturnedRecordset(recordset2);
})
.catch(err => handleErrorSomehow(err));
API Reference
{Function} pgpw.init(name, cnDetails, [retryOptions])
Initialize a database object. You can initialize only one database object with a given name per application. Otherwise an Error
is thrown.
Parameters:
-
{String} name
- Name of database object for further reference. -
{Object} cnDetails
- Database connection details-
{String} cnDetails.host
- Database host. Might be a host name or a valid IPv4/IPv6 address; -
{Integer} cnDetails.port
- Database port; -
{String} cnDetails.database
- Database name; -
{String} cnDetails.user
- Database role/user name; -
{String} cnDetails.password
- User password.
-
-
{Object} retryOptions
- Optional description of retry strategy to be applied to every database operation made on the database object being created, by default. It is possible to amend the strategy or cancel it competely for a particular operation viaoptions.retry
parameter. If any non-falsey value is provided here, this must be an object of correct shape with each and every property carring a correct value. Otherwise, the function throws a TypeError.-
{Integer} retryOptions.initialDelayMs
- Initial delay in milliseconds for first retry; -
{Integer} retryOptions.maxAttempts
- Maximum amount of retry attempts; -
{Number} exponent
- Multiplicator for calculation of delay for next attempt. E.g., given initialDelayMs = 200 and exponent = 2, the first retry attempt will be made in 200 ms after initial operation failure, the second retry attempt will be made in 200 * 2 = 400 ms after the first attempt, the third retry attempt will be made in 400 * 2 = 800 ms after the second attempt, and so on...
-
Returns: void
.
{Function} pgpw(name)
Returns database object by name.
Parameter:
-
{String} name
- Database object name.
Returns: {Object}
.
Database object (db)
{Function} db.execFunc(name, params, [options])
Execute a single database function in a separate database connection.
Parameters:
-
{String} name
- Database function name. -
{Array<Any>} params
- Database function parameters. -
{Object} [options]
- Additional options for execution-
{Object} [retry]
- Retry options which override, for this execution, default retry strategy the database object has been initialised with. For shape of object refer topgpw.init
retryOptions
parameter description.-
If this property is not provided, the default retry strategy (if any) will be applied.
-
If any value, other then boolean false value, is provided here, this must be an object of correct shape with each and every property carring a correct value. Otherwise, the function throws a TypeError.
-
If the boolean false value is provided, default retry strategy will be ignored and no retries will be performed.
-
-
Returns: {Promise<Any>}
- Data returned by the function.
{Function} db.transaction([transactionMode], implementor, [options])
Parameters:
-
{Object} transactionMode
- Optional. Transaction mode settings (see details below). -
{Function} implementor(t)
- Function that is called to execute tasks. Whithin the function, it is possible to use any type of task provided by the pg-promise library (i.e., t.func(), t.query(), t.task(), etc) including nested transactions (t.tx()). The function must return either-
t.batch([task1, task2, ...])
- for parallel task execution, or -
t.sequence(source)
- for serial task execution.
-
-
{Object} [options]
- Additional options for execution-
{Object} [retry]
- Retry options which override, for this execution, default retry strategy the database object has been initialised with. For shape of object refer topgpw.init
retryOptions
parameter description.-
If this property is not provided, the default retry strategy (if any) will be applied.
-
If any value, other then boolean false value, is provided here, this must be an object of correct shape with each and every property carring a correct value. Otherwise, the function throws a TypeError.
-
If the boolean false value is provided, default retry strategy will be ignored and no retries will be performed.
-
-
Returns: {Promise<Any>}
- Result of executing the tasks.
{Object} transactionMode
Transaction mode settings This parameter allows to extend the default BEGIN
command which starts a transaction.
Properties:
-
{db.isolationLevel} isolationLevel
- Optional. Transaction isolation level. If not provided, or the provided value doesn't matchdb.isolationLevel
enumeration, defaults to having no effect on transaction mode. -
{Boolean} isReadOnly
- Optional. Sets transaction access mode. The exact valuetrue
addsBEGIN READ ONLY
. The exact valuefalse
addsBEGIN READ WRITE
. Any other value (includingundefined
, i.e. the property is not provided) adds nothing to theBEGIN
command. -
{Boolean} isDeferrable
- Optional. Sets transaction deferrable mode. The exact valuetrue
addsBEGIN DEFERRABLE
. The exact valuefalse
addsBEGIN NOT DEFERRABLE
. Any other value (includingundefined
, i.e. the property is not provided) adds nothing to theBEGIN
command. This setting has effect only whenisolationLevel = db.isolationLevel.serializable
andisReadOnly = true
, or else it is ignored.
db.isolationLevel
enumeration
-
serializable
- AddsISOLATION LEVEL SERIALIZABLE
; -
repeatableRead
- AddsISOLATION LEVEL REPEATABLE READ
; -
readCommitted
- AddsISOLATION LEVEL READ COMMITTED
.
Error handling
The Promise returned by execFunc
and transaction
methods rejects with the following VErrors:
-
DatabaseUnavailable
- when connecting to database fails. The error provides the followinginfo
properties:-
address {String}
- Database host; -
port {Number}
- Database port.
-
-
UnexpectedError
- when any other error occurred.
Stubs API
Stubs API provides for an unified way to stub the methods exposed by the module.
pgpw.stubs([{Object} customStubs])
Returns function to be used in place of "required" pgpw
module.
Generally, you don't need to call this function directly - it will be called by the require proxy you use in you test suit (think of proxyquire). When called returns an object with the following keys:
-
{Function|Array<Function>} execFunc
- default or custom stub/stubs fordb.execFunc(...)
method; -
{Function|Array<Function>} transaction
- default or custom stub/stubs fordb.transaction(...)
method.
You can pass your custom stubs through the optional parameter customStubs
which accepts an object with keys execFunc
and transaction
. All keys are optional. For every key that is not provided, the default stub is returned. Default stub returns Promise
that resolves with null
.
If you pass an array of stubs to either execFunc
of transaction
, these stubs will be executed sequentially on sequential calls to the relevant method in your production code. If your production code calls a method more times that the number of stubs in the array, the last stub is executed on all subsequent calls.
Usage Example
Say you want to unit test the following module:
/* my-module.js */
const db = require('pgpw')('my_db_name');
module.exports = async function retrieveDataFromDb () {
const [ resultOne, resultTwo ] = await Promise.all([
db.execFunc('func_1', ['param1', 'param2']),
db.execFunc('func_2', ['param3', 'param4'])
]);
const [ resultTree, resultFour ] = await db.transaction(t => t.batch([
t.func('func_3', ['param5']),
t.func('func_4', ['param6'])
]));
// Logic to test
if (resultOne.length && resultTwo.length && resultTree.length && resultFour.length) {
return 'foo';
} else {
return 'bar';
}
};
Test file:
const test = require('tape');
const proxyquire = require('proxyquire').noPreserveCache();
const dbStub = require('pgpw').stubs;
const getSelf = ({
execFuncStub = [
() => Promise.resolve([]),
() => Promise.resolve([''])
]
} = {}) => proxyquire('./my-module', {
'pgpw': dbStub({ execFunc: execFuncStub })
});
test('all queries return data', async t => {
try {
const self = proxyquire('./my-module', {
'pgpw': dbStub({
execFunc: [
() => Promise.resolve(['item1', 'item2']),
() => Promise.resolve(['item3', 'item4'])
],
transaction: () => Promise.resolve([
['item5', 'item6'],
['item7', 'item8']
])
})
});
const result = await self();
t.equal(result, 'foo', 'should return foo');
t.end();
} catch (err) {
t.end(err);
}
});