Skip to content

Commit

Permalink
Simplify url parse for strings, update CHANGELOG
Browse files Browse the repository at this point in the history
  • Loading branch information
slvrtrn committed Feb 29, 2024
1 parent 3c0fead commit 6ff872b
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 46 deletions.
27 changes: 25 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ See also: [readonly documentation](https://clickhouse.com/docs/en/operations/set
- Added `url` configuration parameter. It is intended to replace the deprecated `host`, which was already supposed to be passed a valid URL.
- It is now possible to configure most of the client instance parameters with a URL. The URL format is `http[s]://[username:password@]hostname:port[/database][?param1=value1&param2=value2]`. The name of a particular parameter is supposed to reflect its path in the config options interface. The following parameters are supported:

- `readonly` - boolean.
- `readonly` - boolean. See below [1].
- `application_id` - non-empty string.
- `session_id` - non-empty string.
- `request_timeout` - non-negative number.
Expand All @@ -52,10 +52,33 @@ See also: [readonly documentation](https://clickhouse.com/docs/en/operations/set
- `compression_response` - boolean.
- `log_level` - allowed values: `OFF`, `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`.
- `keep_alive_enabled` - boolean.
- `clickhouse_settings_*` - see below [2].
- `additional_headers_*` - see below [3].
- (Node.js only) `keep_alive_socket_ttl` - non-negative number.
- (Node.js only) `keep_alive_retry_on_expired_socket` - boolean.

For booleans, valid values will be `true`/`1` and `false`/`0`.
[1] For booleans, valid values will be `true`/`1` and `false`/`0`.

[2] Any parameter prefixed with `clickhouse_settings_` will have this prefix removed and the rest added to client's `clickhouse_settings`. For example, `?clickhouse_settings_async_insert=1&clickhouse_settings_wait_for_async_insert=1` will be the same as:

```ts
createClient({
clickhouse_settings: {
async_insert: 1,
wait_for_async_insert: 1,
},
})
```

[3] Similar to [2], but for `additional_headers` configuration. For example, `?additional_headers_x-clickhouse-auth=foobar` will be an equivalent of:

```ts
createClient({
additional_headers: {
'x-clickhouse-auth': 'foobar',
},
})
```

URL will _always_ overwrite the hardcoded values and a warning will be logged in this case.

Expand Down
82 changes: 44 additions & 38 deletions packages/client-common/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,21 +282,23 @@ export function loadConfigOptionsFromURL(
config.readonly = booleanConfigURLValue({ key, value })
break
case 'application':
config.application = stringConfigURLValue({ key, value })
config.application = value
break
case 'session_id':
config.session_id = stringConfigURLValue({ key, value })
config.session_id = value
break
case 'request_timeout':
config.request_timeout = numberConfigURLValue({ min: 0 })({
config.request_timeout = numberConfigURLValue({
key,
value,
min: 0,
})
break
case 'max_open_connections':
config.max_open_connections = numberConfigURLValue({ min: 1 })({
config.max_open_connections = numberConfigURLValue({
key,
value,
min: 1,
})
break
case 'compression_request':
Expand All @@ -318,9 +320,10 @@ export function loadConfigOptionsFromURL(
if (config.log === undefined) {
config.log = {}
}
config.log.level = enumConfigURLValue(ClickHouseLogLevel)({
config.log.level = enumConfigURLValue({
key,
value,
enumObject: ClickHouseLogLevel,
})
break
case 'keep_alive_enabled':
Expand Down Expand Up @@ -350,55 +353,58 @@ export function loadConfigOptionsFromURL(
return [clickHouseURL, config]
}

type KV = { key: string; value: string }
type ConfigURLValue<T = unknown> = (kv: KV) => T

export function booleanConfigURLValue({ key, value }: KV): boolean {
export function booleanConfigURLValue({
key,
value,
}: {
key: string
value: string
}): boolean {
const trimmed = value.trim()
if (trimmed === 'true' || trimmed === '1') return true
if (trimmed === 'false' || trimmed === '0') return false
throw new Error(`${key} has invalid boolean value: ${trimmed}`)
}

export function numberConfigURLValue({
key,
value,
min,
max,
}: {
key: string
value: string
min?: number
max?: number
}): ConfigURLValue<number> {
return ({ key, value }: KV): number => {
const trimmed = value.trim()
const number = Number(trimmed)
if (isNaN(number))
throw new Error(`${key} has invalid number value: ${trimmed}`)
if (min !== undefined && number < min) {
throw new Error(`${key} value is less than minimum: ${trimmed}`)
}
if (max !== undefined && number > max) {
throw new Error(`${key} value is greater than maximum: ${trimmed}`)
}
return number
}): number {
const trimmed = value.trim()
const number = Number(trimmed)
if (isNaN(number))
throw new Error(`${key} has invalid number value: ${trimmed}`)
if (min !== undefined && number < min) {
throw new Error(`${key} value is less than minimum: ${trimmed}`)
}
}

export function enumConfigURLValue<Enum, Key extends string>(enumObject: {
[key in Key]: Enum
}): ConfigURLValue<Enum> {
const values = Object.keys(enumObject).filter((item) => isNaN(Number(item)))
return ({ key, value }: KV): Enum => {
const trimmed = value.trim()
if (!values.includes(trimmed)) {
throw new Error(`${key} has invalid value: ${trimmed}`)
}
return enumObject[trimmed as Key]
if (max !== undefined && number > max) {
throw new Error(`${key} value is greater than maximum: ${trimmed}`)
}
return number
}

export function stringConfigURLValue({ key, value }: KV): string {
export function enumConfigURLValue<Enum, Key extends string>({
key,
value,
enumObject,
}: {
key: string
value: string
enumObject: {
[key in Key]: Enum
}
}): Enum {
const values = Object.keys(enumObject).filter((item) => isNaN(Number(item)))
const trimmed = value.trim()
if (trimmed === '') {
throw new Error(`${key} has empty value.`)
if (!values.includes(trimmed)) {
throw new Error(`${key} has invalid value: ${trimmed}`)
}
return trimmed
return enumObject[trimmed as Key]
}
1 change: 0 additions & 1 deletion packages/client-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export {
booleanConfigURLValue,
enumConfigURLValue,
numberConfigURLValue,
stringConfigURLValue,
} from './config'
export {
withCompressionHeaders,
Expand Down
4 changes: 3 additions & 1 deletion packages/client-node/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,10 @@ export function createClient(
nodeConfig.keep_alive = {}
}
nodeConfig.keep_alive.socket_ttl = numberConfigURLValue({
key,
value,
min: 0,
})({ key, value })
})
handledParams.add(key)
break
default:
Expand Down
8 changes: 4 additions & 4 deletions packages/client-web/__tests__/unit/web_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ import { createClient } from '../../src'

describe('[Web] createClient', () => {
it('throws on incorrect "host" config value', () => {
expect(() => createClient({ host: 'foo' })).toThrowError(
expect(() => createClient({ url: 'foo' })).toThrowError(
'Configuration parameter "host" contains malformed url.'
)
})

it('should not mutate provided configuration', async () => {
const config: BaseClickHouseClientConfigOptions<unknown> = {
host: 'http://localhost',
const config: BaseClickHouseClientConfigOptions = {
url: 'http://localhost',
}
createClient(config)
// initial configuration is not overridden by the defaults we assign
// when we transform the specified config object to the connection params
expect(config).toEqual({
host: 'http://localhost',
url: 'http://localhost',
})
})
})

0 comments on commit 6ff872b

Please sign in to comment.