Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pg-connection-string): ClientConfig helper functions #3128

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/pg-connection-string/README.md
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
@@ -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
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
@@ -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"')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like it should've thrown an error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is current behavior according to:

subject.database.should.equal('my[db]', 'must to be escaped and unescaped trough "my%5Bdb%5D"')

clientConfig.client_encoding.should.equal('utf8')
})
})