'use strict';
const http = require('http');
const https = require('https');
const HTTP_PORT = 9000;
const TCP_PORT = 9009;
const HTTP = 'http';
const HTTPS = 'https';
const TCP = 'tcp';
const TCPS = 'tcps';
const ON = 'on';
const OFF = 'off';
const UNSAFE_OFF = 'unsafe_off';
/** @classdesc
* <a href="Sender.html">Sender</a> configuration options. <br>
* <br>
* Properties of the object are initialized through a configuration string. <br>
* The configuration string has the following format: <i><protocol>::<key>=<value><key>=<value>...;</i> <br>
* The keys are case-sensitive, the trailing semicolon is optional. <br>
* The values are validated, and an error is thrown if the format is invalid. <br>
* <br>
* Connection and protocol options
* <ul>
* <li> <b>protocol</b>: <i>enum, accepted values: http, https, tcp, tcps</i> - The protocol used to communicate with the server. <br>
* When <i>https</i> or <i>tcps</i> used, the connection is secured with TLS encryption.
* </li>
* <li> addr: <i>string</i> - Hostname and port, separated by colon. This key is mandatory, but the port part is optional. <br>
* If no port is specified, a default will be used. <br>
* When the protocol is HTTP/HTTPS, the port defaults to 9000. When the protocol is TCP/TCPS, the port defaults to 9009. <br>
* <br>
* Examples: <i>http::addr=localhost:9000</i>, <i>https::addr=localhost:9000</i>, <i>http::addr=localhost</i>, <i>tcp::addr=localhost:9009</i>
* </li>
* </ul>
* <br>
* Authentication options
* <ul>
* <li> username: <i>string</i> - Used for authentication. <br>
* For HTTP, Basic Authentication requires the <i>password</i> option. <br>
* For TCP with JWK token authentication, <i>token</i> option is required.
* </li>
* <li> password: <i>string</i> - Password for HTTP Basic authentication, should be accompanied by the <i>username</i> option.
* </li>
* <li> token: <i>string</i> - For HTTP with Bearer authentication, this is the bearer token. <br>
* For TCP with JWK token authentication, this is the private key part of the JWK token,
* and must be accompanied by the <i>username</i> option.
* </li>
* </ul>
* <br>
* TLS options
* <ul>
* <li> tls_verify: <i>enum, accepted values: on, unsafe_off</i> - When the HTTPS or TCPS protocols are selected, TLS encryption is used. <br>
* By default, the Sender will verify the server's certificate, but this check can be disabled by setting this option to <i>off</i>. This is useful
* non-production environments where self-signed certificates might be used, but should be avoided in production if possible.
* </li>
* <li> tls_ca: <i>string</i> - Path to a file containing the root CA's certificate in PEM format. <br>
* Can be useful when self-signed certificates are used, otherwise should not be set.
* </li>
* </ul>
* <br>
* Auto flush options
* <ul>
* <li> auto_flush: <i>enum, accepted values: on, off</i> - The Sender automatically flushes the buffer by default. This can be switched off
* by setting this option to <i>off</i>. <br>
* When disabled, the flush() method of the Sender has to be called explicitly to make sure data is sent to the server. <br>
* Manual buffer flushing can be useful, especially when we want to use transactions. When the HTTP protocol is used, each flush results in a single HTTP
* request, which becomes a single transaction on the server side. The transaction either succeeds, and all rows sent in the request are
* inserted; or it fails, and none of the rows make it into the database.
* </li>
* <li> auto_flush_rows: <i>integer</i> - The number of rows that will trigger a flush. When set to 0, row-based flushing is disabled. <br>
* The Sender will default this parameter to 75000 rows when HTTP protocol is used, and to 600 in case of TCP protocol.
* </li>
* <li> auto_flush_interval: <i>integer</i> - The number of milliseconds that will trigger a flush, default value is 1000.
* When set to 0, interval-based flushing is disabled. <br>
* Note that the setting is checked only when a new row is added to the buffer. There is no timer registered to flush the buffer automatically.
* </li>
* </ul>
* <br>
* Buffer sizing options
* <ul>
* <li> init_buf_size: <i>integer</i> - Initial buffer size, defaults to 64 KiB in the Sender.
* </li>
* <li> max_buf_size: <i>integer</i> - Maximum buffer size, defaults to 100 MiB in the Sender. <br>
* If the buffer would need to be extended beyond the maximum size, an error is thrown.
* </li>
* </ul>
* <br>
* HTTP request specific options
* <ul>
* <li> request_timeout: <i>integer</i> - The time in milliseconds to wait for a response from the server, set to 10 seconds by default. <br>
* This is in addition to the calculation derived from the <i>request_min_throughput</i> parameter.
* </li>
* <li> request_min_throughput: <i>integer</i> - Minimum expected throughput in bytes per second for HTTP requests, set to 100 KiB/s seconds by default. <br>
* If the throughput is lower than this value, the connection will time out. This is used to calculate an additional
* timeout on top of <i>request_timeout</i>. This is useful for large requests. You can set this value to 0 to disable this logic.
* </li>
* <li> retry_timeout: <i>integer</i> - The time in milliseconds to continue retrying after a failed HTTP request, set to 10 seconds by default. <br>
* The interval between retries is an exponential backoff starting at 10ms and doubling after each failed attempt up to a maximum of 1 second.
* </li>
* </ul>
* <br>
* Other options
* <ul>
* <li> max_name_len: <i>integer</i> - The maximum length of a table or column name, the Sender defaults this parameter to 127. <br>
* Recommended to use the same setting as the server, which also uses 127 by default.
* </li>
* <li> copy_buffer: <i>enum, accepted values: on, off</i> - By default, the Sender creates a new buffer for every flush() call,
* and the data to be sent to the server is copied into this new buffer.
* Setting the flag to <i>off</i> results in reusing the same buffer instance for each flush() call. <br>
* Use this flag only if calls to the client are serialised.
* </li>
* </ul>
*/
class SenderOptions {
protocol;
addr;
host; // derived from addr
port; // derived from addr
// replaces `auth` and `jwk` options
username;
password;
token;
token_x; // allowed, but ignored
token_y; // allowed, but ignored
auto_flush;
auto_flush_rows;
auto_flush_interval;
// replaces `copyBuffer` option
copy_buffer;
request_min_throughput;
request_timeout;
retry_timeout;
// replaces `bufferSize` option
init_buf_size;
max_buf_size;
tls_verify;
tls_ca;
tls_roots; // not supported
tls_roots_password; // not supported
max_name_len;
log;
agent;
/**
* Creates a Sender options object by parsing the provided configuration string.
*
* @param {string} configurationString - Configuration string. <br>
* @param {object} extraOptions - Optional extra configuration. <br>
* - 'log' is a logging function used by the <a href="Sender.html">Sender</a>. <br>
* Prototype: <i>(level: 'error'|'warn'|'info'|'debug', message: string) => void</i>. <br>
* - 'agent' is a custom http/https agent used by the <a href="Sender.html">Sender</a> when http/https transport is used. <br>
* A <i>http.Agent</i> or <i>https.Agent</i> object is expected.
*/
constructor(configurationString, extraOptions = undefined) {
parseConfigurationString(this, configurationString);
if (extraOptions) {
if (extraOptions.log && typeof extraOptions.log !== 'function') {
throw new Error('Invalid logging function');
}
this.log = extraOptions.log;
if (extraOptions.agent && !(extraOptions.agent instanceof http.Agent) && !(extraOptions.agent instanceof https.Agent)) {
throw new Error('Invalid http/https agent');
}
this.agent = extraOptions.agent;
}
}
/**
* Creates a Sender options object by parsing the provided configuration string.
*
* @param {string} configurationString - Configuration string. <br>
* @param {object} extraOptions - Optional extra configuration. <br>
* - 'log' is a logging function used by the <a href="Sender.html">Sender</a>. <br>
* Prototype: <i>(level: 'error'|'warn'|'info'|'debug', message: string) => void</i>. <br>
* - 'agent' is a custom http/https agent used by the <a href="Sender.html">Sender</a> when http/https transport is used. <br>
* A <i>http.Agent</i> or <i>https.Agent</i> object is expected.
*
* @return {SenderOptions} A Sender configuration object initialized from the provided configuration string.
*/
static fromConfig(configurationString, extraOptions = undefined) {
return new SenderOptions(configurationString, extraOptions);
}
/**
* Creates a Sender options object by parsing the configuration string set in the <b>QDB_CLIENT_CONF</b> environment variable.
*
* @param {object} extraOptions - Optional extra configuration. <br>
* - 'log' is a logging function used by the <a href="Sender.html">Sender</a>. <br>
* Prototype: <i>(level: 'error'|'warn'|'info'|'debug', message: string) => void</i>. <br>
* - 'agent' is a custom http/https agent used by the <a href="Sender.html">Sender</a> when http/https transport is used. <br>
* A <i>http.Agent</i> or <i>https.Agent</i> object is expected.
*
* @return {SenderOptions} A Sender configuration object initialized from the <b>QDB_CLIENT_CONF</b> environment variable.
*/
static fromEnv(extraOptions = undefined) {
return SenderOptions.fromConfig(process.env.QDB_CLIENT_CONF, extraOptions);
}
}
function parseConfigurationString(options, configString) {
if (!configString) {
throw new Error('Configuration string is missing or empty');
}
const position = parseProtocol(options, configString);
parseSettings(options, configString, position);
parseAddress(options);
parseBufferSizes(options);
parseAutoFlushOptions(options);
parseTlsOptions(options);
parseRequestTimeoutOptions(options);
parseMaxNameLength(options);
parseCopyBuffer(options);
}
function parseSettings(options, configString, position) {
let index = configString.indexOf(';', position);
while (index > -1) {
if (index + 1 < configString.length && configString.charAt(index + 1) === ';') {
index = configString.indexOf(';', index + 2);
continue;
}
parseSetting(options, configString, position, index);
position = index + 1;
index = configString.indexOf(';', position);
}
if (position < configString.length) {
parseSetting(options, configString, position, configString.length);
}
}
function parseSetting(options, configString, position, index) {
const setting = configString.slice(position, index).replaceAll(';;', ';');
const equalsIndex = setting.indexOf('=');
if (equalsIndex < 0) {
throw new Error(`Missing '=' sign in '${setting}'`);
}
const key = setting.slice(0, equalsIndex);
const value = setting.slice(equalsIndex + 1);
validateConfigKey(key);
validateConfigValue(key, value);
options[key] = value;
}
const ValidConfigKeys = [
'addr',
'username', 'password', 'token', 'token_x', 'token_y',
'auto_flush', 'auto_flush_rows', 'auto_flush_interval',
'copy_buffer',
'request_min_throughput', 'request_timeout', 'retry_timeout',
'init_buf_size', 'max_buf_size',
'max_name_len',
'tls_verify', 'tls_ca', 'tls_roots', 'tls_roots_password'
];
function validateConfigKey(key) {
if (!ValidConfigKeys.includes(key)) {
throw new Error(`Unknown configuration key: '${key}'`);
}
}
function validateConfigValue(key, value) {
if (!value) {
throw new Error(`Invalid configuration, value is not set for '${key}'`);
}
for (let i = 0; i < value.length; i++) {
const unicode = value.codePointAt(i);
if (unicode < 0x20 || (unicode > 0x7E && unicode < 0xA0)) {
throw new Error(`Invalid configuration, control characters are not allowed: '${value}'`);
}
}
}
function parseProtocol(options, configString) {
let index = configString.indexOf('::');
if (index < 0) {
throw new Error('Missing protocol, configuration string format: \'protocol::key1=value1;key2=value2;key3=value3;\'');
}
options.protocol = configString.slice(0, index);
switch (options.protocol) {
case HTTP:
case HTTPS:
case TCP:
case TCPS:
break;
default:
throw new Error(`Invalid protocol: '${options.protocol}', accepted protocols: 'http', 'https', 'tcp', 'tcps'`);
}
return index + 2;
}
function parseAddress(options) {
if (!options.addr) {
throw new Error('Invalid configuration, \'addr\' is required');
}
const index = options.addr.indexOf(':');
if (index < 0) {
options.host = options.addr;
switch (options.protocol) {
case HTTP:
case HTTPS:
options.port = HTTP_PORT;
return;
case TCP:
case TCPS:
options.port = TCP_PORT;
return;
default:
throw new Error(`Invalid protocol: '${options.protocol}', accepted protocols: 'http', 'https', 'tcp', 'tcps'`);
}
}
options.host = options.addr.slice(0, index);
if (!options.host) {
throw new Error(`Host name is required`);
}
const portStr = options.addr.slice(index + 1);
if (!portStr) {
throw new Error(`Port is required`);
}
options.port = Number(portStr);
if (isNaN(options.port)) {
throw new Error(`Invalid port: '${portStr}'`);
}
if (!Number.isInteger(options.port) || options.port < 1) {
throw new Error(`Invalid port: ${options.port}`);
}
}
function parseBufferSizes(options) {
parseInteger(options, 'init_buf_size', 'initial buffer size', 1);
parseInteger(options, 'max_buf_size', 'max buffer size', 1);
}
function parseAutoFlushOptions(options) {
parseBoolean(options, 'auto_flush', 'auto flush');
parseInteger(options, 'auto_flush_rows', 'auto flush rows', 0);
parseInteger(options, 'auto_flush_interval', 'auto flush interval', 0);
}
function parseTlsOptions(options) {
parseBoolean(options, 'tls_verify', 'TLS verify', UNSAFE_OFF);
if (options.tls_roots || options.tls_roots_password) {
throw new Error('\'tls_roots\' and \'tls_roots_password\' options are not supported, please, ' +
'use the \'tls_ca\' option or the NODE_EXTRA_CA_CERTS environment variable instead');
}
}
function parseRequestTimeoutOptions(options) {
parseInteger(options, 'request_min_throughput', 'request min throughput', 1);
parseInteger(options, 'request_timeout', 'request timeout', 1);
parseInteger(options, 'retry_timeout', 'retry timeout', 0);
}
function parseMaxNameLength(options) {
parseInteger(options, 'max_name_len', 'max name length', 1);
}
function parseCopyBuffer(options) {
parseBoolean(options, 'copy_buffer', 'copy buffer');
}
function parseBoolean(options, property, description, offValue = OFF) {
if (options[property]) {
const property_str = options[property];
switch (property_str) {
case ON:
options[property] = true;
break;
case offValue:
options[property] = false;
break;
default:
throw new Error(`Invalid ${description} option: '${property_str}'`);
}
}
}
function parseInteger(options, property, description, lowerBound) {
if (options[property]) {
const property_str = options[property];
options[property] = Number(property_str);
if (isNaN(options[property])) {
throw new Error(`Invalid ${description} option, not a number: '${property_str}'`);
}
if (!Number.isInteger(options[property]) || options[property] < lowerBound) {
throw new Error(`Invalid ${description} option: ${options[property]}`);
}
}
}
exports.SenderOptions = SenderOptions;
exports.HTTP = HTTP;
exports.HTTPS = HTTPS;
exports.TCP = TCP;
exports.TCPS = TCPS;