@juit/pgproxy-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # PostgreSQL Proxy over HTTP and WebSockets (CLI interface)
2
+
3
+ This package provides a simple command line interface to run the PGProxy Server.
4
+
5
+ * [Usage](#usage)
6
+ * [Configuration Files](#configuration-files)
7
+ * [Main Section](#main-section)
8
+ * [Pool Section](#pool-section)
9
+ * [Environment Variables](#environment-variables)
10
+ * [PGProxy](https://github.com/juitnow/juit-pgproxy/blob/main/README.md)
11
+ * [Copyright Notice](https://github.com/juitnow/juit-pgproxy/blob/main/NOTICE.md)
12
+ * [License](https://github.com/juitnow/juit-pgproxy/blob/main/NOTICE.md)
13
+
14
+ ### Usage
15
+
16
+ ```
17
+ Usage:
18
+
19
+ pgproxy-server [--options ...] [config file]
20
+
21
+ Options:
22
+
23
+ --debug Enable verbose logging.
24
+ --help Show this help page and exit.
25
+ --version Show version information and exit.
26
+
27
+ [config file] An optional configuration file (in ".ini" format).
28
+
29
+ Environment variables:
30
+
31
+ HTTP Server:
32
+
33
+ PGPROXYSECRET The secret used to authenticate clients.
34
+ PGPROXYADDRESS The address where this server will be bound to.
35
+ PGPROXYPORT The port number where this server will be bound to.
36
+ PGPROXYHEALTHCHECK Path for the unauthenticated health check GET request.
37
+
38
+ Connection Pool:
39
+
40
+ PGPOOLMINSIZE Minimum number of connections to keep in the pool.
41
+ PGPOOLMAXSIZE Maximum number of connections to keep in the pool.
42
+ PGPOOLIDLECONN Maximum number of idle connections in the pool.
43
+ PGPOOLACQUIRETIMEOUT Number of seconds after which 'acquire()' will fail.
44
+ PGPOOLBORROWTIMEOUT Maximum seconds a connection can be borrowed for.
45
+ PGPOOLRETRYINTERVAL Seconds to wait after connection creation failed.
46
+
47
+ PostgreSQL:
48
+
49
+ PGHOST Name of host to connect to.
50
+ PGPORT Port number to connect to at the server host.
51
+ PGDATABASE The database name.
52
+ PGUSER PostgreSQL user name to connect as.
53
+ PGPASSWORD Password to be used if the server demands authentication.
54
+
55
+ See also: https://www.postgresql.org/docs/current/libpq-envars.html
56
+
57
+ Remarks:
58
+
59
+ Environment variables will also be read from a ".env" file in the current
60
+ directory (if such file exists).
61
+
62
+ See also: https://github.com/motdotla/dotenv
63
+ ```
64
+
65
+ ### Configuration Files
66
+
67
+ The configuration file(s) used by the command line interface are in `ini` format
68
+ and contain two parts: the configuration of the PGProxy server (main section),
69
+ and the configuration of the connection pool to PostgreSQL (`[pool]` section).
70
+
71
+ For example
72
+
73
+ ```ini
74
+ secret = mySuperSecret
75
+ port = 12345
76
+
77
+ [pool]
78
+ database = myDatabaseName
79
+ user = myUser
80
+ password = myPasswor
81
+ host = localhost
82
+ port = 5432
83
+ ```
84
+
85
+ #### Main section
86
+
87
+ In the _main_ section the following options are available to configure the
88
+ PGProxy server:
89
+
90
+ * `secret`: The secret used to authenticate clients.
91
+ * `address`: The address where this server will be bound to.
92
+ * `port`: The port number where this server will be bound to.
93
+ * `backlog`: The maximum length of the queue of pending connections.
94
+ * `healthCheck`: The path used to provide stats and a healthcheck via GET requests.
95
+
96
+ Furthermore, underlying NodeJS HTTP server the following options are available.
97
+ Refer to the [Node JS documentation](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener)
98
+ for information on their behaviour.
99
+
100
+ * `connectionsCheckingInterval`
101
+ * `highWaterMark`
102
+ * `insecureHTTPParser`
103
+ * `joinDuplicateHeaders`
104
+ * `keepAlive`
105
+ * `keepAliveInitialDelay`
106
+ * `keepAliveTimeout`
107
+ * `maxHeaderSize`
108
+ * `noDelay`
109
+ * `requestTimeout`
110
+
111
+ #### Pool section
112
+
113
+ In the `[pool]` section the following options are available to configure the
114
+ connection pool:
115
+
116
+ * `minimumPoolSize`: The minimum number of connections to keep in the pool
117
+ (default: `0`).
118
+ * `maximumPoolSize`: The maximum number of connections to keep in the pool
119
+ (default: `20`).
120
+ * `maximumIdleConnections`: The maximum number of idle connections that can be
121
+ sitting in the pool (default: the average between `minimumPoolSize` and
122
+ `maximumPoolSize`).
123
+ * `acquireTimeout`: The number of seconds after which an `acquire()` call will
124
+ fail (default: `30` sec.).
125
+ * `borrowTimeout`: The maximum number of seconds a connection can be borrowed
126
+ for (default: `120` sec.).
127
+ * `retryInterval`: The number of seconds to wait after the creation of a
128
+ connection failed (default: `5` sec.).
129
+ * `validateOnBorrow`: Whether to validate connections on borrow or not (value
130
+ must be either `true` or `false`, default: `true`).
131
+
132
+ Furthermore, to configure the _connections_ to PostgreSQL:
133
+
134
+ * `database`: The database name.
135
+ * `host`: Name of host to connect to.
136
+ * `address`: IPv4 or IPv6 numeric IP address of host to connect to.
137
+ * `port`: Port number to connect to at the server host.
138
+ * `user`: PostgreSQL user name to connect as.
139
+ * `password`: Password to be used if the server demands password authentication.
140
+ * `connectTimeout`: Maximum wait for connection, in seconds.
141
+ * `applicationName`: The `application_name` as it will appear in `pg_stat_activity`.
142
+ * `keepalives`: Controls whether client-side TCP keepalives are used.
143
+ * `keepalivesIdle`: The number of seconds of inactivity after which TCP should send a keepalive message to the server.
144
+ * `keepalivesInterval`: The number of seconds after which a TCP keepalive message that is not acknowledged by the server should be retransmitted.
145
+ * `keepalivesCount`: The number of TCP keepalives that can be lost before the client's connection to the server is considered dead.
146
+ * `sslMode`: This option determines whether or with what priority a secure SSL
147
+ TCP/IP connection will be negotiated with the server. There are six modes:
148
+ * `disable`: only try a non-SSL connection
149
+ * `allow`: first try a non-SSL connection; if that fails, try an SSL
150
+ connection
151
+ * `prefer` _(default)_: first try an SSL connection; if that fails, try a
152
+ non-SSL connection
153
+ * `require`: only try an SSL connection. If a root CA file is present, verify
154
+ the certificate in the same way as if verify-ca was specified
155
+ * `verify-ca`: only try an SSL connection, and verify that the server
156
+ certificate is issued by a trusted certificate authority (CA)
157
+ * `verify-full`: only try an SSL connection, verify that the server
158
+ certificate is issued by a trusted CA and that the server host name matches
159
+ that in the certificate
160
+ * `sslCompression`: If set to `true` (default), data sent over SSL connections
161
+ will be compressed.
162
+ * `sslCertFile`: The file name of the client SSL certificate.
163
+ * `sslKeyFile`: The location for the secret key used for the client certificate.
164
+ * `sslRootCertFile`: The name of a file containing SSL certificate authority
165
+ (CA) certificate(s).
166
+ * `sslCrlFile`: The file name of the SSL certificate revocation list (CRL).
167
+ * `kerberosServiceName`: Kerberos service name to use when authenticating with
168
+ Kerberos 5 or GSSAPI.
169
+ * `gssLibrary`: GSS library to use for GSSAPI authentication.
170
+
171
+ ### Environment Variables
172
+
173
+ Most options can also be configured through environment variables as follows:
174
+
175
+ * _main._`secret` => `PGPROXYSECRET`
176
+ * _main._`address` => `PGPROXYADDRESS`
177
+ * _main._`port` => `PGPROXYPORT`
178
+ * _main._`healthCheck` => `PGPROXYHEALTHCHECK`
179
+
180
+ For the connection pool:
181
+
182
+ * `pool.minimumPoolSize` => `PGPOOLMINSIZE`
183
+ * `pool.maximumPoolSize` => `PGPOOLMAXSIZE`
184
+ * `pool.maximumIdleConnections` => `PGPOOLIDLECONN`
185
+ * `pool.acquireTimeout` => `PGPOOLACQUIRETIMEOUT`
186
+ * `pool.borrowTimeout` => `PGPOOLBORROWTIMEOUT`
187
+ * `pool.retryInterval` => `PGPOOLRETRYINTERVAL`
188
+ * `pool.validateOnBorrow` => `PGPOOLVALIDATEONBORROW`
189
+
190
+ And for the connection to PosgreSQL:
191
+
192
+ * `pool.host` => `PGHOST`
193
+ * `pool.port` => `PGPORT`
194
+ * `pool.database` => `PGDATABASE`
195
+ * `pool.user` => `PGUSER`
196
+ * `pool.password` => `PGPASSWORD`
197
+
198
+ For more see also: https://www.postgresql.org/docs/current/libpq-envars.html
package/dist/cli.d.mts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.mjs ADDED
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+
3
+ // cli.mts
4
+ import { readFileSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { Server } from "@juit/pgproxy-server";
8
+ import { config as dotEnvConfig } from "dotenv";
9
+ import { parse } from "ini";
10
+ import { boolean, number, object, oneOf, optional, string, validate } from "justus";
11
+ import yargsParser from "yargs-parser";
12
+ var logger = {
13
+ debug: function(...args2) {
14
+ if (debug)
15
+ console.log("[DEBUG]", ...args2);
16
+ },
17
+ info: function(...args2) {
18
+ console.log("[INFO] ", ...args2);
19
+ },
20
+ warn: function(...args2) {
21
+ console.log("[WARN] ", ...args2);
22
+ },
23
+ error: function(...args2) {
24
+ console.log("[ERROR]", ...args2);
25
+ }
26
+ };
27
+ function showHelp() {
28
+ console.log(`
29
+ Usage:
30
+
31
+ pgproxy-server [--options ...] [config file]
32
+
33
+ Options:
34
+
35
+ --debug Enable verbose logging.
36
+ --help Show this help page and exit.
37
+ --version Show version information and exit.
38
+
39
+ [config file] An optional configuration file (in ".ini" format).
40
+
41
+ Environment variables:
42
+
43
+ HTTP Server:
44
+
45
+ PGPROXYSECRET The secret used to authenticate clients.
46
+ PGPROXYADDRESS The address where this server will be bound to.
47
+ PGPROXYPORT The port number where this server will be bound to.
48
+ PGPROXYHEALTHCHECK Path for the unauthenticated health check GET request.
49
+
50
+ Connection Pool:
51
+
52
+ PGPOOLMINSIZE Minimum number of connections to keep in the pool.
53
+ PGPOOLMAXSIZE Maximum number of connections to keep in the pool.
54
+ PGPOOLIDLECONN Maximum number of idle connections in the pool.
55
+ PGPOOLACQUIRETIMEOUT Seconds after which 'acquire()' will fail.
56
+ PGPOOLBORROWTIMEOUT Maximum seconds a connection can be borrowed for.
57
+ PGPOOLRETRYINTERVAL Seconds to wait after connection creation failed.
58
+ PGPOOLVALIDATEONBORROW Whether to validate connnections on borrow or not.
59
+
60
+ PostgreSQL:
61
+
62
+ PGHOST Name of host to connect to.
63
+ PGPORT Port number to connect to at the server host.
64
+ PGDATABASE The database name.
65
+ PGUSER PostgreSQL user name to connect as.
66
+ PGPASSWORD Password to be used if the server demands authentication.
67
+
68
+ See also: https://www.postgresql.org/docs/current/libpq-envars.html
69
+
70
+ Remarks:
71
+
72
+ Environment variables will also be read from a ".env" file in the current
73
+ directory (if such file exists).
74
+
75
+ See also: https://github.com/motdotla/dotenv
76
+ `);
77
+ process.exit(1);
78
+ }
79
+ function showVersion() {
80
+ const path = fileURLToPath(import.meta.url);
81
+ const file = resolve(path, "..", "..", "package.json");
82
+ const data = readFileSync(file, "utf-8");
83
+ const json = JSON.parse(data);
84
+ console.log(`v${json.version}`);
85
+ process.exit(1);
86
+ }
87
+ var booleanValidator = boolean({ fromString: true });
88
+ var numberValidator = number({ fromString: true, minimum: 0 });
89
+ var stringValidator = string({ minLength: 1 });
90
+ var poolValidator = object({
91
+ /* Connection pool options */
92
+ acquireTimeout: optional(numberValidator),
93
+ borrowTimeout: optional(numberValidator),
94
+ maximumIdleConnections: optional(numberValidator),
95
+ maximumPoolSize: optional(numberValidator),
96
+ minimumPoolSize: optional(numberValidator),
97
+ retryInterval: optional(numberValidator),
98
+ validateOnBorrow: optional(booleanValidator),
99
+ /* LibPQ options */
100
+ address: optional(stringValidator),
101
+ applicationName: optional(stringValidator),
102
+ connectTimeout: optional(numberValidator),
103
+ database: optional(stringValidator),
104
+ gssLibrary: optional("gssapi"),
105
+ host: optional(stringValidator),
106
+ keepalives: optional(booleanValidator),
107
+ keepalivesCount: optional(numberValidator),
108
+ keepalivesIdle: optional(numberValidator),
109
+ keepalivesInterval: optional(numberValidator),
110
+ kerberosServiceName: optional(stringValidator),
111
+ password: optional(stringValidator),
112
+ port: optional(numberValidator),
113
+ sslCertFile: optional(stringValidator),
114
+ sslCompression: optional(booleanValidator),
115
+ sslCrlFile: optional(stringValidator),
116
+ sslKeyFile: optional(stringValidator),
117
+ sslMode: optional(oneOf("disable", "allow", "prefer", "require", "verify-ca", "verify-full")),
118
+ sslRootCertFile: optional(stringValidator),
119
+ user: optional(stringValidator)
120
+ });
121
+ var serverValidator = object({
122
+ /* Proxy server */
123
+ secret: string({ minLength: 8 }),
124
+ address: optional(string({ minLength: 1 })),
125
+ port: optional(numberValidator),
126
+ backlog: optional(numberValidator),
127
+ healthCheck: optional(stringValidator),
128
+ /* Connection pool & database */
129
+ pool: optional(poolValidator),
130
+ /* Node HTTP server options */
131
+ connectionsCheckingInterval: optional(numberValidator),
132
+ highWaterMark: optional(numberValidator),
133
+ insecureHTTPParser: optional(booleanValidator),
134
+ joinDuplicateHeaders: optional(booleanValidator),
135
+ keepAlive: optional(booleanValidator),
136
+ keepAliveInitialDelay: optional(numberValidator),
137
+ keepAliveTimeout: optional(numberValidator),
138
+ maxHeaderSize: optional(numberValidator),
139
+ noDelay: optional(booleanValidator),
140
+ requestTimeout: optional(numberValidator)
141
+ });
142
+ function readConfigs(files2) {
143
+ dotEnvConfig();
144
+ const config = {
145
+ secret: process.env.PGPROXYSECRET,
146
+ address: process.env.PGPROXYADDRESS,
147
+ port: process.env.PGPROXYPORT || "54321",
148
+ healthCheck: process.env.PGPROXYHEALTHCHECK
149
+ };
150
+ for (const file of files2) {
151
+ const text = readFileSync(file, "utf-8");
152
+ Object.assign(config, parse(text));
153
+ }
154
+ return validate(serverValidator, config);
155
+ }
156
+ var { _: args, ...opts } = yargsParser(process.argv.slice(2), {
157
+ configuration: {
158
+ "camel-case-expansion": false,
159
+ "strip-aliased": true,
160
+ "strip-dashed": true
161
+ },
162
+ alias: {
163
+ "debug": ["d"],
164
+ "help": ["h"],
165
+ "version": ["v"]
166
+ },
167
+ boolean: [
168
+ "debug",
169
+ "help",
170
+ "version"
171
+ ]
172
+ });
173
+ var files = args.map((arg) => `${arg}`);
174
+ var debug = !!opts.debug;
175
+ if (opts.help)
176
+ showHelp();
177
+ if (opts.version)
178
+ showVersion();
179
+ for (const key of Object.keys(opts)) {
180
+ switch (key) {
181
+ case "debug":
182
+ case "help":
183
+ case "version":
184
+ continue;
185
+ default:
186
+ logger.error(`Unsupported / unknown option: --${key}
187
+ `);
188
+ showHelp();
189
+ }
190
+ }
191
+ var options;
192
+ try {
193
+ options = readConfigs(files);
194
+ } catch (error) {
195
+ logger.error(error.message);
196
+ process.exit(1);
197
+ }
198
+ var server = new Server(logger, options);
199
+ await server.start();
200
+ logger.info(`DB proxy server running with PID ${process.pid}`);
201
+ process.on("SIGINT", () => server.stop().then(() => process.exitCode = 0).catch((error) => {
202
+ logger.error(error);
203
+ process.exit(1);
204
+ }));
205
+ process.on("SIGTERM", () => server.stop().then(() => process.exitCode = 0).catch((error) => {
206
+ logger.error(error);
207
+ process.exit(1);
208
+ }));
209
+ process.on("SIGHUP", () => {
210
+ try {
211
+ options = readConfigs(files);
212
+ } catch (error) {
213
+ logger.error(error.message);
214
+ logger.error("Not restarting running server");
215
+ }
216
+ server.stop().then(() => {
217
+ server = new Server(logger, options);
218
+ return server.start();
219
+ }).catch((error) => {
220
+ logger.error(error);
221
+ process.exit(1);
222
+ });
223
+ });
224
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/cli.mts"],
4
+ "mappings": "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",
5
+ "names": ["args", "files"]
6
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@juit/pgproxy-cli",
3
+ "version": "1.0.0",
4
+ "bin": {
5
+ "pgproxy-server": "./dist/cli.mjs"
6
+ },
7
+ "author": "Juit Developers <developers@juit.com>",
8
+ "license": "Apache-2.0",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+ssh://git@github.com/juitnow/juit-pgproxy.git"
12
+ },
13
+ "keywords": [
14
+ "database",
15
+ "pg",
16
+ "pool",
17
+ "postgres",
18
+ "proxy"
19
+ ],
20
+ "bugs": {
21
+ "url": "https://github.com/juitnow/juit-pgproxy/issues"
22
+ },
23
+ "homepage": "https://github.com/juitnow/juit-pgproxy#readme",
24
+ "dependencies": {
25
+ "@juit/pgproxy-server": "^1.0.0",
26
+ "dotenv": "^16.3.1",
27
+ "ini": "^4.1.1",
28
+ "justus": "^0.5.7",
29
+ "ws": "^8.15.1",
30
+ "yargs-parser": "^21.1.1"
31
+ },
32
+ "directories": {
33
+ "test": "test"
34
+ },
35
+ "files": [
36
+ "*.md",
37
+ "dist/",
38
+ "src/"
39
+ ]
40
+ }
package/src/cli.mts ADDED
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+ /* coverage ignore file */
3
+ /* eslint-disable no-console */
4
+
5
+ import { readFileSync } from 'node:fs'
6
+ import { resolve } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+
9
+ import { Server } from '@juit/pgproxy-server'
10
+ import { config as dotEnvConfig } from 'dotenv'
11
+ import { parse } from 'ini'
12
+ import { boolean, number, object, oneOf, optional, string, validate } from 'justus'
13
+ import yargsParser from 'yargs-parser'
14
+
15
+ import type { Logger } from '@juit/pgproxy-pool'
16
+ import type { ServerOptions } from '@juit/pgproxy-server'
17
+
18
+ /* Basic logger, used throughout */
19
+ const logger: Logger = {
20
+ debug: function(...args: any[]): void {
21
+ if (debug) console.log('[DEBUG]', ...args)
22
+ },
23
+ info: function(...args: any[]): void {
24
+ console.log('[INFO] ', ...args)
25
+ },
26
+ warn: function(...args: any[]): void {
27
+ console.log('[WARN] ', ...args)
28
+ },
29
+ error: function(...args: any[]): void {
30
+ console.log('[ERROR]', ...args)
31
+ },
32
+ }
33
+
34
+ /* ========================================================================== *
35
+ * HELP AND VERSION *
36
+ * ========================================================================== */
37
+
38
+ function showHelp(): never {
39
+ console.log(`
40
+ Usage:
41
+
42
+ pgproxy-server [--options ...] [config file]
43
+
44
+ Options:
45
+
46
+ --debug Enable verbose logging.
47
+ --help Show this help page and exit.
48
+ --version Show version information and exit.
49
+
50
+ [config file] An optional configuration file (in ".ini" format).
51
+
52
+ Environment variables:
53
+
54
+ HTTP Server:
55
+
56
+ PGPROXYSECRET The secret used to authenticate clients.
57
+ PGPROXYADDRESS The address where this server will be bound to.
58
+ PGPROXYPORT The port number where this server will be bound to.
59
+ PGPROXYHEALTHCHECK Path for the unauthenticated health check GET request.
60
+
61
+ Connection Pool:
62
+
63
+ PGPOOLMINSIZE Minimum number of connections to keep in the pool.
64
+ PGPOOLMAXSIZE Maximum number of connections to keep in the pool.
65
+ PGPOOLIDLECONN Maximum number of idle connections in the pool.
66
+ PGPOOLACQUIRETIMEOUT Seconds after which 'acquire()' will fail.
67
+ PGPOOLBORROWTIMEOUT Maximum seconds a connection can be borrowed for.
68
+ PGPOOLRETRYINTERVAL Seconds to wait after connection creation failed.
69
+ PGPOOLVALIDATEONBORROW Whether to validate connnections on borrow or not.
70
+
71
+ PostgreSQL:
72
+
73
+ PGHOST Name of host to connect to.
74
+ PGPORT Port number to connect to at the server host.
75
+ PGDATABASE The database name.
76
+ PGUSER PostgreSQL user name to connect as.
77
+ PGPASSWORD Password to be used if the server demands authentication.
78
+
79
+ See also: https://www.postgresql.org/docs/current/libpq-envars.html
80
+
81
+ Remarks:
82
+
83
+ Environment variables will also be read from a ".env" file in the current
84
+ directory (if such file exists).
85
+
86
+ See also: https://github.com/motdotla/dotenv
87
+ `)
88
+ process.exit(1)
89
+ }
90
+
91
+ function showVersion(): never {
92
+ const path = fileURLToPath(import.meta.url)
93
+ const file = resolve(path, '..', '..', 'package.json')
94
+ const data = readFileSync(file, 'utf-8')
95
+ const json = JSON.parse(data)
96
+ console.log(`v${json.version}`)
97
+ process.exit(1)
98
+ }
99
+
100
+ /* ========================================================================== *
101
+ * CONFIGURATION *
102
+ * ========================================================================== */
103
+
104
+ /* Validation */
105
+ const booleanValidator = boolean({ fromString: true })
106
+ const numberValidator = number({ fromString: true, minimum: 0 })
107
+ const stringValidator = string({ minLength: 1 })
108
+
109
+ /* PoolOptions validator */
110
+ const poolValidator = object({
111
+ /* Connection pool options */
112
+ acquireTimeout: optional(numberValidator),
113
+ borrowTimeout: optional(numberValidator),
114
+ maximumIdleConnections: optional(numberValidator),
115
+ maximumPoolSize: optional(numberValidator),
116
+ minimumPoolSize: optional(numberValidator),
117
+ retryInterval: optional(numberValidator),
118
+ validateOnBorrow: optional(booleanValidator),
119
+ /* LibPQ options */
120
+ address: optional(stringValidator),
121
+ applicationName: optional(stringValidator),
122
+ connectTimeout: optional(numberValidator),
123
+ database: optional(stringValidator),
124
+ gssLibrary: optional('gssapi'),
125
+ host: optional(stringValidator),
126
+ keepalives: optional(booleanValidator),
127
+ keepalivesCount: optional(numberValidator),
128
+ keepalivesIdle: optional(numberValidator),
129
+ keepalivesInterval: optional(numberValidator),
130
+ kerberosServiceName: optional(stringValidator),
131
+ password: optional(stringValidator),
132
+ port: optional(numberValidator),
133
+ sslCertFile: optional(stringValidator),
134
+ sslCompression: optional(booleanValidator),
135
+ sslCrlFile: optional(stringValidator),
136
+ sslKeyFile: optional(stringValidator),
137
+ sslMode: optional(oneOf('disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full')),
138
+ sslRootCertFile: optional(stringValidator),
139
+ user: optional(stringValidator),
140
+ })
141
+
142
+ /* ServerOptions validator */
143
+ const serverValidator = object({
144
+ /* Proxy server */
145
+ secret: string({ minLength: 8 }),
146
+ address: optional(string({ minLength: 1 })),
147
+ port: optional(numberValidator),
148
+ backlog: optional(numberValidator),
149
+ healthCheck: optional(stringValidator),
150
+ /* Connection pool & database */
151
+ pool: optional(poolValidator),
152
+ /* Node HTTP server options */
153
+ connectionsCheckingInterval: optional(numberValidator),
154
+ highWaterMark: optional(numberValidator),
155
+ insecureHTTPParser: optional(booleanValidator),
156
+ joinDuplicateHeaders: optional(booleanValidator),
157
+ keepAlive: optional(booleanValidator),
158
+ keepAliveInitialDelay: optional(numberValidator),
159
+ keepAliveTimeout: optional(numberValidator),
160
+ maxHeaderSize: optional(numberValidator),
161
+ noDelay: optional(booleanValidator),
162
+ requestTimeout: optional(numberValidator),
163
+ })
164
+
165
+ function readConfigs(files: string[]): ServerOptions {
166
+ /* First, parse any and all ".env" file in CWD for environment variables */
167
+ dotEnvConfig()
168
+
169
+ /* Base configuration fron environment variables */
170
+ const config: Record<string, string | undefined> = {
171
+ secret: process.env.PGPROXYSECRET,
172
+ address: process.env.PGPROXYADDRESS,
173
+ port: process.env.PGPROXYPORT || '54321',
174
+ healthCheck: process.env.PGPROXYHEALTHCHECK,
175
+ }
176
+
177
+ /* Parse command line files */
178
+ for (const file of files) {
179
+ const text = readFileSync(file, 'utf-8')
180
+ Object.assign(config, parse(text))
181
+ }
182
+
183
+ /* Validate and return our options */
184
+ return validate(serverValidator, config)
185
+ }
186
+
187
+
188
+ /* ========================================================================== *
189
+ * STARTUP *
190
+ * ========================================================================== */
191
+
192
+ /* Then parse our command line arguments */
193
+ const { _: args, ...opts } = yargsParser(process.argv.slice(2), {
194
+ configuration: {
195
+ 'camel-case-expansion': false,
196
+ 'strip-aliased': true,
197
+ 'strip-dashed': true,
198
+ },
199
+ alias: {
200
+ 'debug': [ 'd' ],
201
+ 'help': [ 'h' ],
202
+ 'version': [ 'v' ],
203
+ },
204
+ boolean: [
205
+ 'debug',
206
+ 'help',
207
+ 'version',
208
+ ],
209
+ })
210
+
211
+ /* Process each option, one by one */
212
+ const files = args.map((arg) => `${arg}`)
213
+ const debug = !! opts.debug
214
+ if (opts.help) showHelp()
215
+ if (opts.version) showVersion()
216
+ for (const key of Object.keys(opts)) {
217
+ switch (key) {
218
+ case 'debug':
219
+ case 'help':
220
+ case 'version':
221
+ continue
222
+ default:
223
+ logger.error(`Unsupported / unknown option: --${key}\n`)
224
+ showHelp()
225
+ }
226
+ }
227
+
228
+ /* Read our configs */
229
+ let options: ServerOptions
230
+ try {
231
+ options = readConfigs(files)
232
+ } catch (error: any) {
233
+ logger.error(error.message)
234
+ process.exit(1)
235
+ }
236
+
237
+ /* Start our server */
238
+ let server = new Server(logger, options)
239
+ await server.start()
240
+ logger.info(`DB proxy server running with PID ${process.pid}`)
241
+
242
+ /* Gracefully terminate on CTRL-C or "kill -TERM $PID" */
243
+ process.on('SIGINT', () => server.stop()
244
+ .then(() => process.exitCode = 0)
245
+ .catch((error) => {
246
+ logger.error(error)
247
+ process.exit(1)
248
+ }))
249
+ process.on('SIGTERM', () => server.stop()
250
+ .then(() => process.exitCode = 0)
251
+ .catch((error) => {
252
+ logger.error(error)
253
+ process.exit(1)
254
+ }))
255
+
256
+ /* Reload configurations and restart server on "kill -HUP $PID" */
257
+ process.on('SIGHUP', () => {
258
+ try {
259
+ options = readConfigs(files)
260
+ } catch (error: any) {
261
+ logger.error(error.message)
262
+ logger.error('Not restarting running server')
263
+ }
264
+
265
+ server.stop()
266
+ .then(() => {
267
+ server = new Server(logger, options)
268
+ return server.start()
269
+ })
270
+ .catch((error) => {
271
+ logger.error(error)
272
+ process.exit(1) // critical, let systemd handle restarts!
273
+ })
274
+ })