connection-string
Version:
Advanced URL Connection String parser + generator.
286 lines (285 loc) • 10.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConnectionString = void 0;
const types_1 = require("./types");
const static_1 = require("./static");
const errInvalidDefaults = `Invalid "defaults" parameter: `;
class ConnectionString {
/**
* Safe read-accessor to the first host's full name (hostname + port).
*/
get host() {
return this.hosts?.[0].toString();
}
/**
* Safe read-accessor to the first host's name (without port).
*/
get hostname() {
return this.hosts?.[0].name;
}
/**
* Safe read-accessor to the first host's port.
*/
get port() {
return this.hosts?.[0].port;
}
/**
* Safe read-accessor to the first host's type.
*/
get type() {
return this.hosts?.[0].type;
}
/**
* Constructor.
*
* @param cs - connection string (can be empty).
*
* @param defaults - optional defaults, which can also be set
* explicitly, via method setDefaults.
*/
constructor(cs, defaults) {
cs = cs ?? '';
if (typeof cs !== 'string') {
throw new TypeError(`Invalid connection string: ${JSON.stringify(cs)}`);
}
if (typeof (defaults ?? {}) !== 'object') {
throw new TypeError(errInvalidDefaults + JSON.stringify(defaults));
}
cs = cs.trim();
(0, static_1.validateUrl)(cs); // will throw, if failed
// Extracting the protocol:
let m = cs.match(/^(.*)?:\/\//);
if (m) {
const p = m[1]; // protocol name
if (p) {
const m2 = p.match(/^([a-z]+[a-z0-9+-.:]*)/i);
if (p && (!m2 || m2[1] !== p)) {
throw new Error(`Invalid protocol name: ${p}`);
}
this.protocol = p;
}
cs = cs.substring(m[0].length);
}
// Extracting user + password:
m = cs.match(/^([\w-_.+!*'()$%]*):?([\w-_.+!*'()$%~]*)@/);
if (m) {
if (m[1]) {
this.user = (0, static_1.decode)(m[1]);
}
if (m[2]) {
this.password = (0, static_1.decode)(m[2]);
}
cs = cs.substring(m[0].length);
}
// Extracting hosts details:
// (if it starts with `/`, it is the first path segment, i.e. no hosts specified)
if (cs[0] !== '/') {
const endOfHosts = cs.search(/[\/?]/);
const hosts = (endOfHosts === -1 ? cs : cs.substring(0, endOfHosts)).split(',');
hosts.forEach(h => {
const host = (0, static_1.parseHost)(h);
if (host) {
if (!this.hosts) {
this.hosts = [];
}
this.hosts.push(host);
}
});
if (endOfHosts >= 0) {
cs = cs.substring(endOfHosts);
}
}
// Extracting the path:
m = cs.match(/\/([\w-_.+!*'()$%]+)/g);
if (m) {
this.path = m.map(s => (0, static_1.decode)(s.substring(1)));
}
// Extracting parameters:
const idx = cs.indexOf('?');
if (idx !== -1) {
cs = cs.substring(idx + 1);
m = cs.match(/([\w-_.+!*'()$%]+)=([\w-_.+!*'()$%,]+)/g);
if (m) {
const params = {};
m.forEach(s => {
const a = s.split('=');
const prop = (0, static_1.decode)(a[0]);
const val = a[1].split(',').map(static_1.decode);
if (prop in params) {
if (Array.isArray(params[prop])) {
params[prop].push(...val);
}
else {
params[prop] = [params[prop], ...val];
}
}
else {
params[prop] = val.length > 1 ? val : val[0];
}
});
this.params = params;
}
}
if (defaults) {
this.setDefaults(defaults);
}
}
/**
* Parses a host name into an object, which then can be passed into `setDefaults`.
*
* It returns `null` only when no valid host recognized.
*/
static parseHost(host) {
return (0, static_1.parseHost)(host, true);
}
/**
* Converts this object into a valid connection string.
*/
toString(options) {
let s = this.protocol ? `${this.protocol}://` : ``;
const opts = options || {};
if (this.user || this.password) {
if (this.user) {
s += (0, static_1.encode)(this.user, opts);
}
if (this.password) {
s += ':';
const h = opts.passwordHash;
if (h) {
const code = (typeof h === 'string' && h[0]) || '#';
s += code.repeat(this.password.length);
}
else {
s += (0, static_1.encode)(this.password, opts);
}
}
s += '@';
}
if (Array.isArray(this.hosts)) {
s += this.hosts.map(h => (0, static_1.fullHostName)(h, options)).join();
}
if (Array.isArray(this.path)) {
this.path.forEach(seg => {
s += `/${(0, static_1.encode)(seg, opts)}`;
});
}
if (this.params && typeof this.params === 'object') {
const params = [];
for (const a in this.params) {
let value = this.params[a];
value = Array.isArray(value) ? value : [value];
value = value.map((v) => {
return (0, static_1.encode)(typeof v === 'string' ? v : JSON.stringify(v), opts);
}).join();
if (opts.plusForSpace) {
value = value.replace(/%20/g, '+');
}
params.push(`${(0, static_1.encode)(a, opts)}=${value}`);
}
if (params.length) {
s += `?${params.join('&')}`;
}
}
return s;
}
/**
* Applies default parameters, and returns itself.
*/
setDefaults(defaults) {
if (!defaults || typeof defaults !== 'object') {
throw new TypeError(errInvalidDefaults + JSON.stringify(defaults));
}
if (!('protocol' in this) && (0, static_1.hasText)(defaults.protocol)) {
this.protocol = defaults.protocol && defaults.protocol.trim();
}
// Missing default `hosts` are merged with the existing ones:
if (Array.isArray(defaults.hosts)) {
const hosts = Array.isArray(this.hosts) ? this.hosts : [];
const dhHosts = defaults.hosts.filter(d => d && typeof d === 'object');
dhHosts.forEach(dh => {
const dhName = (0, static_1.hasText)(dh.name) ? dh.name.trim() : undefined;
const h = { name: dhName, port: dh.port, type: dh.type };
let found = false;
for (let i = 0; i < hosts.length; i++) {
const thisHost = (0, static_1.fullHostName)(hosts[i]), defHost = (0, static_1.fullHostName)(h);
if (thisHost.toLowerCase() === defHost.toLowerCase()) {
found = true;
break;
}
}
if (!found) {
const obj = {};
if (h.name) {
if (h.type && h.type in types_1.HostType) {
obj.name = h.name;
obj.type = h.type;
}
else {
const t = (0, static_1.parseHost)(h.name, true);
if (t) {
obj.name = t.name;
obj.type = t.type;
}
}
}
const p = h.port;
if (typeof p === 'number' && p > 0 && p < 65536) {
obj.port = p;
}
if (obj.name || obj.port) {
Object.defineProperty(obj, 'toString', {
value: (options) => (0, static_1.fullHostName)(obj, options)
});
hosts.push(obj);
}
}
});
if (hosts.length) {
this.hosts = hosts;
}
}
if (!('user' in this) && (0, static_1.hasText)(defaults.user)) {
this.user = defaults.user.trim();
}
if (!('password' in this) && (0, static_1.hasText)(defaults.password)) {
this.password = defaults.password.trim();
}
// Since the order of `path` segments is usually important, we set default
// `path` segments as they are, but only when they are missing completely:
if (!('path' in this) && Array.isArray(defaults.path)) {
const s = defaults.path.filter(static_1.hasText);
if (s.length) {
this.path = s;
}
}
// Missing default `params` are merged with the existing ones:
if (defaults.params && typeof defaults.params === 'object') {
const keys = Object.keys(defaults.params);
if (keys.length) {
if (this.params && typeof this.params === 'object') {
for (const a in defaults.params) {
if (!(a in this.params)) {
this.params[a] = defaults.params[a];
}
}
}
else {
this.params = {};
for (const b in defaults.params) {
this.params[b] = defaults.params[b];
}
}
}
}
return this;
}
}
exports.ConnectionString = ConnectionString;
(function () {
// hiding prototype methods, to keep the type signature clean:
['setDefaults', 'toString'].forEach(prop => {
const desc = Object.getOwnPropertyDescriptor(ConnectionString.prototype, prop);
desc.enumerable = false;
Object.defineProperty(ConnectionString.prototype, prop, desc);
});
})();