@kurrent/kurrentdb-client
Version:
KurrentDB gRPC NodeJS Client SDK
238 lines • 9.54 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseConnectionString = void 0;
const constants_1 = require("../constants");
const utils_1 = require("../utils");
const toflatCase = (str) => str.trim().toLowerCase().replace(/[_-]/g, "");
// This wierd key value syntax is the only way I could find ensure that every option in a union is passed
const caseMap = (options) => {
const expected = Object.values(options);
const mapping = new Map(expected.map((option) => [toflatCase(option), option]));
function MapTo(value) {
const flatValue = toflatCase(value);
return mapping.get(flatValue);
}
MapTo.expected = expected;
return MapTo;
};
const mapToNodePreference = caseMap({
[constants_1.LEADER]: constants_1.LEADER,
[constants_1.FOLLOWER]: constants_1.FOLLOWER,
[constants_1.READ_ONLY_REPLICA]: constants_1.READ_ONLY_REPLICA,
[constants_1.RANDOM]: constants_1.RANDOM,
});
const mapToQueryOption = caseMap({
userCertFile: "userCertFile",
userKeyFile: "userKeyFile",
maxDiscoverAttempts: "maxDiscoverAttempts",
connectionName: "connectionName",
defaultDeadline: "defaultDeadline",
discoveryInterval: "discoveryInterval",
gossipTimeout: "gossipTimeout",
nodePreference: "nodePreference",
tls: "tls",
tlsVerifyCert: "tlsVerifyCert",
tlsCAFile: "tlsCAFile",
throwOnAppendFailure: "throwOnAppendFailure",
keepAliveInterval: "keepAliveInterval",
keepAliveTimeout: "keepAliveTimeout",
});
const parseConnectionString = (connectionString) => parseProtocol(connectionString.trim().replace(/\/+$/, ""), 0, {
dnsDiscover: false,
hosts: [],
});
exports.parseConnectionString = parseConnectionString;
const parseProtocol = (connectionString, position, options) => {
let nextPosition = position;
const expected = "kurrentdb://, kurrentdb+discover://, kurrent://, kurrent+discover://, kdb://, or kdb+discover://";
const match = connectionString
.substring(position)
.match(/^(?<protocol>[^:]+):\/\//);
if (match && match.groups?.protocol) {
nextPosition += match[0].length;
if (match.groups.protocol.startsWith("esdb")) {
console.warn(`The 'esdb' protocol is deprecated. Please use 'kurrentdb' instead`);
}
switch (match.groups.protocol) {
case "esdb":
case "kurrentdb":
case "kdb":
case "kurrent": {
return parseCredentials(connectionString, nextPosition, {
...options,
dnsDiscover: false,
});
}
case "esdb+discover":
case "kurrentdb+discover":
case "kdb+discover":
case "kurrent+discover": {
return parseCredentials(connectionString, nextPosition, {
...options,
dnsDiscover: true,
});
}
}
}
throw new ParseError(connectionString, [position, nextPosition], expected);
};
const parseCredentials = (connectionString, position, options) => {
let nextPosition = position;
const expected = "username:password";
const match = connectionString
.substring(position)
.match(/^(?:(?<credentials>[^:]+:[^@]+)@)/);
// This is optional
if (!match) {
return parseHosts(connectionString, nextPosition, options, true);
}
if (match.groups?.credentials) {
nextPosition += match[0].length;
const [username, password] = match.groups.credentials.trim().split(":");
return parseHosts(connectionString, nextPosition, {
...options,
defaultCredentials: {
username: decodeURIComponent(username),
password: decodeURIComponent(password),
},
}, true);
}
throw new ParseError(connectionString, [position, nextPosition], expected);
};
const parseHosts = (connectionString, position, options, mustMatch) => {
let nextPosition = position;
const expected = "host";
const match = connectionString
.substring(position)
.match(/^(?:(?<host>[^$+!?*'(),;[\]{}|"%~#<>=&]+)[,/]?)/);
if (!match && mustMatch) {
throw new ParseError(connectionString, [position, nextPosition], expected);
}
if (match && match.groups?.host) {
nextPosition += match[0].length;
const [address, rawPort = "2113", ...rest] = match
.groups.host.trim()
.split(":");
if (rest.length) {
throw new ParseError(connectionString, [position + `${address}:${rawPort}`.length, nextPosition], ", or ?key=value");
}
const port = parseInt(rawPort.trim());
if (Number.isNaN(port)) {
throw new ParseError(connectionString, [position + `${address}:`.length, nextPosition], "port number");
}
return parseHosts(connectionString, nextPosition, {
...options,
hosts: [...options.hosts, { address, port }],
}, false);
}
return parseSearchParams(connectionString, nextPosition, options, true);
};
const parseSearchParams = (connectionString, position, options, first) => {
if (position === connectionString.length)
return options;
let nextPosition = position;
const expected = `${first ? "?" : "&"}key=value`;
const match = connectionString
.substring(position)
.match(new RegExp(`^(?:[${first ? "?" : "&"}](?<key>[^=]+)=(?<value>[^&?]+))`));
if (!match || !match.groups || !match.groups.key || !match.groups.value) {
throw new ParseError(connectionString, [position, nextPosition], expected);
}
nextPosition += match[0].length;
const keypair = verifyKeyValuePair(match.groups, connectionString, [position, nextPosition]);
return parseSearchParams(connectionString, nextPosition, keypair
? {
...options,
[keypair.key]: keypair.value,
}
: options, false);
};
const verifyKeyValuePair = ({ key: rawKey, value: rawValue }, connectionString, [from, to]) => {
const keyFrom = from + `&${rawKey}=`.length;
const key = mapToQueryOption(rawKey) ?? rawKey;
const value = rawValue.trim();
if (key === "tlsVerifyCert" && value === "false") {
console.warn([
`"tlsVerifyCert" is not currently supported by this client, and will have no effect.`,
`Consider either:`,
` Passing "tlsCAFile" in the connection string.`,
` Setting NODE_EXTRA_CA_CERTS https://nodejs.org/api/cli.html#cli_node_extra_ca_certs_file`,
errorLocationString(connectionString, [from + `&`.length, to]),
].join("\n"));
}
switch (key) {
case "nodePreference": {
const parsedValue = mapToNodePreference(value);
if (!parsedValue) {
throw new ParseError(connectionString, [keyFrom, to], mapToNodePreference.expected.join(" or "));
}
return { key, value: parsedValue };
}
case "connectionName":
case "tlsCAFile": {
return { key, value: decodeURIComponent(value) };
}
case "userCertFile": {
return { key, value: decodeURIComponent(value) };
}
case "userKeyFile": {
return { key, value: decodeURIComponent(value) };
}
case "maxDiscoverAttempts":
case "defaultDeadline":
case "discoveryInterval":
case "gossipTimeout":
case "keepAliveInterval":
case "keepAliveTimeout": {
const parsedValue = parseInt(value);
if (Number.isNaN(parsedValue)) {
throw new ParseError(connectionString, [keyFrom, to], "Integer");
}
return { key, value: parsedValue };
}
case "dnsDiscover":
case "tls":
case "tlsVerifyCert":
case "throwOnAppendFailure": {
if (value !== "true" && value !== "false") {
throw new ParseError(connectionString, [keyFrom, to], "true or false");
}
return { key, value: value === "true" };
}
}
console.warn([
`Unknown option key "${key}", setting will be ignored.`,
errorLocationString(connectionString, [
from + `&`.length,
from + `&${key}`.length,
]),
].join("\n"));
return null;
};
const errorString = (connectionString, [from, to], expected, full) => {
const lines = [];
lines.push(`Unexpected "${connectionString.substring(from, from === to ? from + 1 : to)}" at position ${from}, expected ${expected}.`);
if (full) {
lines.push(errorLocationString(connectionString, [from, to]));
}
return lines.join("\n");
};
const errorLocationString = (connectionString, [from, to]) => {
const lines = [];
lines.push(connectionString);
lines.push(`${" ".repeat(from)}${"^".repeat(Math.max(to - from, 1))}`);
return lines.join("\n");
};
class ParseError extends Error {
connectionString;
location;
expected;
constructor(connectionString, location, expected) {
super(errorString(connectionString, location, expected, false));
this.connectionString = connectionString;
this.location = location;
this.expected = expected;
utils_1.debug.connection(errorString(connectionString, location, expected, true));
}
}
//# sourceMappingURL=parseConnectionString.js.map