pg-promise (part 2)
In this part, we take a closer look at the implementation of pg-promise, dissecting some basic components.
This is the next part of the open-source review of pg-promise[1]. In this part, I want to take a closer look at the overall structure of the project and will also cover some of the larger and major components.
After reading this and part 1, my hope is that you should have enough knowledge to move around the project and understand where to navigate for what. Additionally, you should understand how queries are handled.
Module Diagram of lib/main.js
Following is a high-level "module" diagram of pg-promise's entry point lib/main.js
; this shows the different modules of pg-promise. The coloring shows their root module. The purpose of this is to see which modules main
directly depends on.
Note: this is not all the modules within pg-promise.
Main Module
This function invocation mainly parses the option and sets up what becomes the pgp
instance for use.
The returned instance is actually an anonymous function referred to as const inst
in the implementation; once you invoke the module such as require('pg-promise')({/*opts*/})
you receive the instance held in inst
. This is ultimately what is returned as pgp
. The object is enriched via npm.utils.addReadProp
to have the API as documented.
Although this leaves out a lot of details, the high level flow is depicted in the following:
This takes us to the next module of instance, Database.js
which defines the API to run promised based queries against pg-promise.
Database.js
What is exported is a function that accepts config
and returns the internal Database
object. That is function Database()
.
module.exports = config => {
const npmLocal = config.$npm;
npmLocal.connect = npmLocal.connect || npm.connect(config);
npmLocal.query = npmLocal.query || npm.query(config);
npmLocal.task = npmLocal.task || npm.task(config);
return Database;
};
This is the module that exposes the main methods of interest such as any
, manyOrNone
, tx
, each
just to name a few.
These are implemented via the obj.query
and then Bitmasks are used to indicate expected result and hence that gives us the naming pattern. For example, for oneOrNone
we pass the flags one | none
.
obj.oneOrNone = function (query, values, cb, thisArg) {
//execute the query and specify the type of result via bitmask
const v = obj.query.call(this, query, values, npm.result.one | npm.result.none);
return transform(v, cb, thisArg); //apply the callback via Promise API
};
Lets take a quick glance at manyOrNone
sans comments:
obj.manyOrNone = function (query, values) {
return obj.query.call(this, query, values, npm.result.many | npm.result.none);
};
Very simple implementation. Clean. Let's take a look at query
which is defined at line 228 in database.js
. We find that it calls the query module which is loaded in the $npm pattern described in part 1. This is also used in oneOrNone.
// [...]
// Generic query method:
query(query, values, qrm) {
if (!ctx.db) {
return $p.reject(new Error(npm.text.queryDisconnected));
}
return config.$npm.query.call(this, ctx, query, values, qrm);
},
// [...]
I feel that we have high-level context with regards to how this module works and what it does. To me, the next major module is the query.js
module since all query related methods looked at above references that. Lets take a look.
Query Module
The query module is responsible for running SQL queries against the data source. As we saw above, it's the generic (bitmask based version) of the oneOrNone
, one
, etc methods as known by consumers of pg-promise.
What is exported for context:
module.exports = config => {
return function (ctx, query, values, qrm) {
return $query.call(this, ctx, query, values, qrm, config);
};
};
The high-level flow of this module is as follows:
The module returns a function that, when boiled down, runs the method $query
which runs the query through the pg
library and returns the results as a promise in the desired format. It's the main implementation, if you will, of the different query methods in database.js
, it's behavior being altered by the bitmasks.
Interesting "aside" points
A couple of interesting "aside" points I learned while preparing this part.
- Event-based APIs are used (such as
lib/database.js
instead of all promises). - There is no promise library built in since the implementor passes it in.
- (Exception is Bluebird is used for testing and is thus in
devDependencies
) of the package.json
- (Exception is Bluebird is used for testing and is thus in
Copyright (c) 2015-2018 Vitaly Tomilov, this work is released under the The MIT License ↩︎