@nicferrier/pgmaker v1.1.2
Making Postgres Databases for fun and storage
Postgres is such a neat database, everybody wants one.
But you don't want to end up with a mad mess of databases everywhere. Instead, you can build some centralization of your databases with this tool.
PgMaker is some js to help you build such a service. It runs a single Postgresql server and provides an HTTP API for creating databases on that server.
Consumers wishing to make a database must be pre-registered with PgMaker for that database.
A consumer receives the resulting databases connection details in a callback to the url it has pre-registered with PgMaker.
How to make a PgMaker service
Mostly all you have to do to provide this as a service is provide a keepie configuration reading implementation.
Therefore the minimum service would be a repostory with a package.json:
{
  "name": "pgmaker-service",
  "version": "1.0.0",
  "description": "Just a pgmaker service.",
  "main": "my-pgmaker-service.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 0"
  },
  "author": "me",
  "license": "ISC",
  "dependencies": {
    "@nicferrier/pgmaker": "1.0.0"
  }
}And the file my-pgmaker-service.js would be:
const pgmaker = require("@nicferrier/pgmaker");
const fs = require("fs");
const path = require("path");
if (require.main === module) {
  const keepieConfigFileName = path.join(__dirname, "database-authorizations.json");
  pgmaker({
     apiPort: 8080,
     keepieConfigFn: pgmaker.keepieConfigFileMaker(keepieConfigFileName)
  })
}where the file called database-authorizations.json does exist in the
root of that repository and it's a JSON file that looks like this:
{
   "http://some-server-that-wants-a-db.example.com:8000/password": "the_db_it_wants",
   "http://server-needs-people-db.example.com:6001/receive": "people_db",
   "http://another-server-needs-people-db.example.com:6002/password": "people_db"
}This would allow some-server-that-wants-a-db.example.com to request
the_db_it_wants and receive the password on it's port 8000 on the
path /password and for server-needs-people-db.example.com and
another-server-needs-people-db.example.com to request people_db
and receive it's connection details, including the secret, on their
ports 6001 and 6002 respectively and their respective paths
/receive and /password.
How to update the authorizations list
The PgMaker service does repeatedly poll the keepieConfigFn to read
in the config again. So you could add more authorized parties while
the server is running simply by updating the file.
How to start a TLS server
You can start a TLS server for PgMaker by supplying either of the
options you need to start a tls server, either pfx:
const pkcs12 = ... ;         // Presumably read it from a file or something
const pkcs12password = ... ; // likewise
pgmaker({
   keepieConfigFn: pgmaker.keepieConfigFileMaker(keepieConfigFileName),
   pfx: Buffer.from(pkcs12, "base64"), 
   passphrase: pkcs12password
})or a private key and a cert:
const tlsOpts = {          // Presumably all come from a file
  key: privateKeyInPem,
  cert, 
  ca
};
pgmaker(Object.assign({
   keepieConfigFn: pgmaker.keepieConfigFileMaker(keepieConfigFileName)
}, tlsOpts));By preference this uses pfx certs if it finds the options for them.
If you do not specify tls options then an http server is made.
How to add routes to your PgMaker service
PgMaker provides pass through functions for it's internal express
app's get, post, delete and head so you can write code like
this:
const pkcs12 = ... ;         // Presumably read it from a file or something
const pkcs12password = ... ; // likewise
pgmaker({
   keepieConfigFn: pgmaker.keepieConfigFileMaker(keepieConfigFileName),
   pfx: Buffer.from(pkcs12, "base64"), 
   passphrase: pkcs12password
}).then(pgMakerService => {
   pgMakerService.get("/", function (req,res) {
      res.send("<h1>PgMaker!</h1><p>Create databases easily!</p>");
   });
});See Express documentation for more details on how to write express handlers.
How PgMaker finds your postgres
Postgres might be installed in all sorts of ways. PgMaker expects the following things:
- the postgres binaries are installed somewhere, we execute postgresfrom there- we try the $PG_BINpath
- we try the $PG_HOME/binpath
- we try the $PG_HOME/pgsql/binpath
 
- we try the 
- the postgres libraries are installed somewhere, we set the LD_LIBRARY_PATHto that- we try the $PG_LIBpath
- we try the $PG_HOME/libpath
- we try the $PG_HOME/pgsql/libpath
 
- we try the 
- a rundirectory for containing the temporary unix socket must exist- we try the $PG_TEMPpath
- we try the $PG_RUNpath
- we try the $PG_DATA/runpath
- we try the $PG_HOME/runpath
 
- we try the 
- a datadirectory where we will create the postgres instance- we try the $PG_DATApath
- we try the $PG_HOME/datadirpath
- we try the $PG_HOME/pgsql/datadirpath
- we try the $PG_HOME/datapath
- we try the $PG_HOME/pgsql/datapath
 
- we try the 
Notice that more specific environment variables take precedence over less specific ones.
We don't need a permanent tcp port for postgres, we just allocate one dynamically.
What's the ideal setup for Postgres?
To do any of this you need to have Postgres installed. So first, how do you do that?
Installing a PostgreSQL distribution
In this repository there is a script initdb.sh which creates a
Postgres installation for PgMaker from the official PostgreSQL
project's tarball available for download
here.
This might also work on Windows with a Windows distribution of Postgres.
Another way is to use a yum or apt distribution and unpack the packaged RPM or DPKG file somewhere.
Advantages of these two ways of installing Postgres are:
- you don't need root to do it
- you can choose to install it anywhere
- you can easily run multiple different versions
Another way is to install Postgresql on your operating system. This might sometimes be easier but:
- usually requires root privilege
- sometimes doesn't allow multiple versions
For development purposes, when running outside secure environments, I recommend running a tarball distribution.
For production or inside secure environments (like large enterprises) I recommend downloading an RPM and unpacking it to a location.
Postgres usually does not need any installation time scripts to make it work, so this is the best approach.
And then how to set it up?
The initdb.sh in this repository depends on the structure of the
postgresql tar file which unwraps with a pgsql directory containing
all the binaries, so we have this:
export PG_HOME=${PG_HOME:-./pg-dist}
export PG_DATA=${PG_DATA:-./pg-data}
$PG_HOME/pgsql/bin/initdb -D $PG_DATA -E=UTF8 --locale=C -U postgres
mkdir -p $PG_DATA/runThe run directory needs to be present and it is not normally.
Often $TEMP is used for the run directory by default by
Postgres. However, because postgres creates important files that have
security risks (like unix socket endpoints) in the directory I've
chosen to make it specific.
So, in summary:
- get a postgresql distribution from a tar or RPM
- set PG_HOMEto the base directory of that
- choose a location to install a postgres instance
- set PG_DATAto the base directory of that
- run postgres initdb -D $PG_DATA postgresto create the instance
- mkdir $PG_DATA/runto make the run directory
- start PgMaker
How to make a PgMaker 'client'
The test-server.js file defines how to do that, but it may look a bit complicated in the context of a test so here is a clearer example:
const fs = require("fs");
const os = require("os");
const express = require("express");
const fetch = require("node-fetch");
const app = express();
const thisPort = 7050;
// Handler to receive the connection details
app.post("/my-pg-connection-details", async function (req, res) {
    let dataBuf="";
    req.on("data", chunk => dataBuf = dataBuf + new String(chunk, "utf8"));
    await new Promise((resolve, reject) => req.on("end", resolve));
    const postParams = new url.URLSearchParams(dataBuf);
    // Presumably we'll do something with these params...
    //
    // Perhaps we write them to a settings file and start another app?
    fs.writeFileSync("postgres-connect-options.conf", postParams);
    res.sendStatus(204);
});
// Start the service
const listener = app.listen(thisPort);
// Send a request to wherever PgMaker is, for a database
const response = await fetch(`http://localhost:${pgMakerPort}/db/pg`, {
    method: "POST",
    body: new url.URLSearchParams({
        "receipt-url": `http://${os.hostname()}:${thisPort}/my-pg-connection`,
        "database-name": "nics_test_db"
    })
});
if (response.status !== 202 || response.status !== 200) {
    console.log("there was an error trying to create `nics_test_db`:", response.status);
    listener.close();
}The handler can do many things. It could call a shell script to do more, or a script that is passed in on the command line perhaps.
For this to work, of course, this server with the url:
`http://${os.hostname()}:7050/my-pg-connection`must have been registered with the PgMaker service to be authorized to
make the database, perhaps int the file
database-authorizations.json; the exact mechanism used to store the
authorized urls that can request a database is configurable with the
PgMaker service.