Skip to content

Commit 1b318dc

Browse files
committed
feat(pg-connection-string): ClientConfig helper functions
Two new functions are introduced to make it easy for TypeScript users to use a PostgresSQL connection string with pg Client. Fixes #2280
1 parent 95d7e62 commit 1b318dc

File tree

4 files changed

+205
-0
lines changed

4 files changed

+205
-0
lines changed

packages/pg-connection-string/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,27 @@ The resulting config contains a subset of the following properties:
3535
* `ca`
3636
* any other query parameters (for example, `application_name`) are preserved intact.
3737

38+
### ClientConfig Compatibility for TypeScript
39+
40+
The pg-connection-string `ConnectionOptions` interface is not compatible with the `ClientConfig` interface that [pg.Client](https://node-postgres.com/apis/client) expects. To remedy this, use the `parseIntoClientConfig` function instead of `parse`:
41+
42+
```ts
43+
import { ClientConfig } from 'pg';
44+
import { parseIntoClientConfig } from 'pg-connection-string';
45+
46+
const config: ClientConfig = parseIntoClientConfig('postgres://someuser:somepassword@somehost:381/somedatabase')
47+
```
48+
49+
You can also use `toClientConfig` to convert an existing `ConnectionOptions` interface into a `ClientConfig` interface:
50+
51+
```ts
52+
import { ClientConfig } from 'pg';
53+
import { parse, toClientConfig } from 'pg-connection-string';
54+
55+
const config = parse('postgres://someuser:somepassword@somehost:381/somedatabase')
56+
const clientConfig: ClientConfig = toClientConfig(config)
57+
```
58+
3859
## Connection Strings
3960

4061
The short summary of acceptable URLs is:

packages/pg-connection-string/index.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { ClientConfig } from 'pg'
2+
13
export function parse(connectionString: string): ConnectionOptions
24

35
export interface ConnectionOptions {
@@ -13,3 +15,6 @@ export interface ConnectionOptions {
1315
fallback_application_name?: string
1416
options?: string
1517
}
18+
19+
export function toClientConfig(config: ConnectionOptions): ClientConfig
20+
export function parseIntoClientConfig(connectionString: string): ClientConfig

packages/pg-connection-string/index.js

+56
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,62 @@ function parse(str) {
107107
return config
108108
}
109109

110+
// convert pg-connection-string ssl config to a ClientConfig.ConnectionOptions
111+
function toConnectionOptions(sslConfig) {
112+
const connectionOptions = Object.entries(sslConfig).reduce((c, [key, value]) => {
113+
// we explicitly check for undefined and null instead of `if (value)` because some
114+
// options accept falsy values. Example: `ssl.rejectUnauthorized = false`
115+
if (value !== undefined && value !== null) {
116+
c[key] = value
117+
}
118+
119+
return c
120+
}, {})
121+
122+
return connectionOptions
123+
}
124+
125+
// convert pg-connection-string config to a ClientConfig
126+
function toClientConfig(config) {
127+
const poolConfig = Object.entries(config).reduce((c, [key, value]) => {
128+
if (key === 'ssl') {
129+
const sslConfig = value
130+
131+
if (typeof sslConfig === 'boolean') {
132+
c[key] = sslConfig
133+
} else if (typeof sslConfig === 'object') {
134+
c[key] = toConnectionOptions(sslConfig)
135+
}
136+
} else if (value !== undefined && value !== null) {
137+
if (key === 'port') {
138+
// when port is not specified, it is converted into an empty string
139+
// we want to avoid NaN or empty string as a values in ClientConfig
140+
if (value !== '') {
141+
const v = parseInt(value, 10)
142+
if (isNaN(v)) {
143+
throw new Error(`Invalid ${key}: ${value}`)
144+
}
145+
146+
c[key] = v
147+
}
148+
} else {
149+
c[key] = value
150+
}
151+
}
152+
153+
return c
154+
}, {})
155+
156+
return poolConfig
157+
}
158+
159+
// parses a connection string into ClientConfig
160+
function parseIntoClientConfig(str) {
161+
return toClientConfig(parse(str))
162+
}
163+
110164
module.exports = parse
111165

112166
parse.parse = parse
167+
parse.toClientConfig = toClientConfig
168+
parse.parseIntoClientConfig = parseIntoClientConfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
'use strict'
2+
3+
const chai = require('chai')
4+
const expect = chai.expect
5+
chai.should()
6+
7+
const { parse, toClientConfig, parseIntoClientConfig } = require('../')
8+
9+
describe('toClientConfig', function () {
10+
it('converts connection info', function () {
11+
const config = parse('postgres://brian:pw@boom:381/lala')
12+
const clientConfig = toClientConfig(config)
13+
14+
clientConfig.user.should.equal('brian')
15+
clientConfig.password.should.equal('pw')
16+
clientConfig.host.should.equal('boom')
17+
clientConfig.port.should.equal(381)
18+
clientConfig.database.should.equal('lala')
19+
})
20+
21+
it('converts query params', function () {
22+
const config = parse(
23+
'postgres:///?application_name=TheApp&fallback_application_name=TheAppFallback&client_encoding=utf8&options=-c geqo=off'
24+
)
25+
const clientConfig = toClientConfig(config)
26+
27+
clientConfig.application_name.should.equal('TheApp')
28+
clientConfig.fallback_application_name.should.equal('TheAppFallback')
29+
clientConfig.client_encoding.should.equal('utf8')
30+
clientConfig.options.should.equal('-c geqo=off')
31+
})
32+
33+
it('converts SSL boolean', function () {
34+
const config = parse('pg:///?ssl=true')
35+
const clientConfig = toClientConfig(config)
36+
37+
clientConfig.ssl.should.equal(true)
38+
})
39+
40+
it('converts sslmode=disable', function () {
41+
const config = parse('pg:///?sslmode=disable')
42+
const clientConfig = toClientConfig(config)
43+
44+
clientConfig.ssl.should.equal(false)
45+
})
46+
47+
it('converts sslmode=noverify', function () {
48+
const config = parse('pg:///?sslmode=no-verify')
49+
const clientConfig = toClientConfig(config)
50+
51+
clientConfig.ssl.rejectUnauthorized.should.equal(false)
52+
})
53+
54+
it('converts other sslmode options', function () {
55+
const config = parse('pg:///?sslmode=verify-ca')
56+
const clientConfig = toClientConfig(config)
57+
58+
clientConfig.ssl.should.deep.equal({})
59+
})
60+
61+
it('converts other sslmode options', function () {
62+
const config = parse('pg:///?sslmode=verify-ca')
63+
const clientConfig = toClientConfig(config)
64+
65+
clientConfig.ssl.should.deep.equal({})
66+
})
67+
68+
it('converts ssl cert options', function () {
69+
const connectionString =
70+
'pg:///?sslcert=' +
71+
__dirname +
72+
'/example.cert&sslkey=' +
73+
__dirname +
74+
'/example.key&sslrootcert=' +
75+
__dirname +
76+
'/example.ca'
77+
const config = parse(connectionString)
78+
const clientConfig = toClientConfig(config)
79+
80+
clientConfig.ssl.should.deep.equal({
81+
ca: 'example ca\n',
82+
cert: 'example cert\n',
83+
key: 'example key\n',
84+
})
85+
})
86+
87+
it('converts ssl cert object', function () {
88+
const config = parse('postgres://boom/lala')
89+
config.ssl = {}
90+
config.ssl.rejectUnauthorized = false
91+
const clientConfig = toClientConfig(config)
92+
93+
clientConfig.ssl.should.deep.equal({
94+
rejectUnauthorized: false,
95+
})
96+
})
97+
98+
it('converts unix domain sockets', function () {
99+
const config = parse('socket:/some path/?db=my[db]&encoding=utf8&client_encoding=bogus')
100+
const clientConfig = toClientConfig(config)
101+
clientConfig.host.should.equal('/some path/')
102+
clientConfig.database.should.equal('my[db]', 'must to be escaped and unescaped through "my%5Bdb%5D"')
103+
clientConfig.client_encoding.should.equal('utf8')
104+
})
105+
106+
it('handles invalid port', function () {
107+
const config = parse('postgres://@boom:381/lala')
108+
config.port = 'bogus'
109+
expect(() => toClientConfig(config)).to.throw()
110+
})
111+
})
112+
113+
describe('parseIntoClientConfig', function () {
114+
it('converts url', function () {
115+
const clientConfig = parseIntoClientConfig('postgres://brian:pw@boom:381/lala')
116+
117+
clientConfig.user.should.equal('brian')
118+
clientConfig.password.should.equal('pw')
119+
clientConfig.host.should.equal('boom')
120+
clientConfig.port.should.equal(381)
121+
clientConfig.database.should.equal('lala')
122+
})
123+
})

0 commit comments

Comments
 (0)