Skip to content

Commit

Permalink
feat(pg-connection-string): ClientConfig helper functions
Browse files Browse the repository at this point in the history
Two new functions are introduced to make it easy for TypeScript
users to use a PostgresSQL connection string with pg Client.

Fixes #2280
  • Loading branch information
hjr3 committed Jan 17, 2024
1 parent df0f4d1 commit 385cea3
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 0 deletions.
21 changes: 21 additions & 0 deletions packages/pg-connection-string/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,27 @@ The resulting config contains a subset of the following properties:
* `ca`
* any other query parameters (for example, `application_name`) are preserved intact.

### ClientConfig Compatibility for TypeScript

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`:

```ts
import { ClientConfig } from 'pg';
import { parseIntoClientConfig } from 'pg-connection-string';

const config: ClientConfig = parseIntoClientConfig('postgres://someuser:somepassword@somehost:381/somedatabase')
```

You can also use `toClientConfig` to convert an existing `ConnectionOptions` interface into a `ClientConfig` interface:

```ts
import { ClientConfig } from 'pg';
import { parse, toClientConfig } from 'pg-connection-string';

const config = parse('postgres://someuser:somepassword@somehost:381/somedatabase')
const clientConfig: ClientConfig = toClientConfig(config)
```

## Connection Strings

The short summary of acceptable URLs is:
Expand Down
5 changes: 5 additions & 0 deletions packages/pg-connection-string/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ClientConfig } from 'pg'

export function parse(connectionString: string): ConnectionOptions

export interface ConnectionOptions {
Expand All @@ -13,3 +15,6 @@ export interface ConnectionOptions {
fallback_application_name?: string
options?: string
}

export function toClientConfig(config: ConnectionOptions): ClientConfig
export function parseIntoClientConfig(connectionString: string): ClientConfig
56 changes: 56 additions & 0 deletions packages/pg-connection-string/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,62 @@ function parse(str) {
return config
}

// convert pg-connection-string ssl config to a ClientConfig.ConnectionOptions
function toConnectionOptions(sslConfig) {
const connectionOptions = Object.entries(sslConfig).reduce((c, [key, value]) => {
// we explicitly check for undefined and null instead of `if (value)` because some
// options accept falsy values. Example: `ssl.rejectUnauthorized = false`
if (value !== undefined && value !== null) {
c[key] = value
}

return c
}, {})

return connectionOptions
}

// convert pg-connection-string config to a ClientConfig
function toClientConfig(config) {
const poolConfig = Object.entries(config).reduce((c, [key, value]) => {
if (key === 'ssl') {
const sslConfig = value

if (typeof sslConfig === 'boolean') {
c[key] = sslConfig
} else if (typeof sslConfig === 'object') {
c[key] = toConnectionOptions(sslConfig)
}
} else if (value !== undefined && value !== null) {
if (key === 'port') {
// when port is not specified, it is converted into an empty string
// we want to avoid NaN or empty string as a values in ClientConfig
if (value !== '') {
const v = parseInt(value, 10)
if (isNaN(v)) {
throw new Error(`Invalid ${key}: ${value}`)
}

c[key] = v
}
} else {
c[key] = value
}
}

return c
}, {})

return poolConfig
}

// parses a connection string into ClientConfig
function parseIntoClientConfig(str) {
return toClientConfig(parse(str))
}

module.exports = parse

parse.parse = parse
parse.toClientConfig = toClientConfig
parse.parseIntoClientConfig = parseIntoClientConfig
94 changes: 94 additions & 0 deletions packages/pg-connection-string/test/toClientConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use strict'

const chai = require('chai')
const expect = chai.expect
chai.should()

const { parse, toClientConfig } = require('../')

describe('toClientConfig', function () {
it('converts connection info', function () {
const config = parse('postgres://brian:pw@boom:381/lala')
const clientConfig = toClientConfig(config)

clientConfig.user.should.equal('brian')
clientConfig.password.should.equal('pw')
clientConfig.host.should.equal('boom')
clientConfig.port.should.equal(381)
clientConfig.database.should.equal('lala')
})

it('converts query params', function () {
const config = parse(
'postgres:///?application_name=TheApp&fallback_application_name=TheAppFallback&client_encoding=utf8&options=-c geqo=off'
)
const clientConfig = toClientConfig(config)

clientConfig.application_name.should.equal('TheApp')
clientConfig.fallback_application_name.should.equal('TheAppFallback')
clientConfig.client_encoding.should.equal('utf8')
clientConfig.options.should.equal('-c geqo=off')
})

it('converts SSL boolean', function () {
const config = parse('pg:///?ssl=true')
const clientConfig = toClientConfig(config)

clientConfig.ssl.should.equal(true)
})

it('converts sslmode=disable', function () {
const config = parse('pg:///?sslmode=disable')
const clientConfig = toClientConfig(config)

clientConfig.ssl.should.equal(false)
})

it('converts sslmode=noverify', function () {
const config = parse('pg:///?sslmode=no-verify')
const clientConfig = toClientConfig(config)

clientConfig.ssl.rejectUnauthorized.should.equal(false)
})

it('converts other sslmode options', function () {
const config = parse('pg:///?sslmode=verify-ca')
const clientConfig = toClientConfig(config)

clientConfig.ssl.should.deep.equal({})
})

it('converts other sslmode options', function () {
const config = parse('pg:///?sslmode=verify-ca')
const clientConfig = toClientConfig(config)

clientConfig.ssl.should.deep.equal({})
})

it('converts ssl cert options', function () {
const connectionString =
'pg:///?sslcert=' +
__dirname +
'/example.cert&sslkey=' +
__dirname +
'/example.key&sslrootcert=' +
__dirname +
'/example.ca'
const config = parse(connectionString)
const clientConfig = toClientConfig(config)

clientConfig.ssl.should.deep.equal({
ca: 'example ca\n',
cert: 'example cert\n',
key: 'example key\n',
})
})

it('converts unix domain sockets', function () {
const config = parse('socket:/some path/?db=my[db]&encoding=utf8&client_encoding=bogus')
const clientConfig = toClientConfig(config)
clientConfig.host.should.equal('/some path/')
clientConfig.database.should.equal('my[db]', 'must to be escaped and unescaped through "my%5Bdb%5D"')
clientConfig.client_encoding.should.equal('utf8')
})
})

0 comments on commit 385cea3

Please sign in to comment.