diff --git a/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/MSSQL.js b/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/MSSQL.js index ed75aa7a..55f8f4c6 100644 --- a/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/MSSQL.js +++ b/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/MSSQL.js @@ -1,5 +1,5 @@ const mssql = require("mssql"); -const UrlPattern = require("url-pattern"); +const { ConnectionStringParser } = require("./utils"); class MSSQLConnector { #connected = false; @@ -34,18 +34,17 @@ class MSSQLConnector { } #parseDatabase() { - const connectionPattern = new UrlPattern( - "mssql\\://:username\\::password@*\\::port/:database*" - ); - const match = connectionPattern.match(this.connectionString); - this.database_id = match?.database; + const connectionParser = new ConnectionStringParser({ scheme: "mssql" }); + const parsed = connectionParser.parse(this.connectionString); + + this.database_id = parsed?.endpoint; this.connectionConfig = { ...this.connectionConfig, - user: match?.username, - password: match?.password, - database: match?.database, - server: match?._[0], - port: match?.port ? Number(match.port) : null, + user: parsed?.username, + password: parsed?.password, + database: parsed?.endpoint, + server: parsed?.hosts[0]?.host, + port: parsed?.hosts[0]?.port, }; } diff --git a/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/MySQL.js b/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/MySQL.js index d9982ab3..5434e7d6 100644 --- a/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/MySQL.js +++ b/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/MySQL.js @@ -1,5 +1,5 @@ const mysql = require("mysql2/promise"); -const UrlPattern = require("url-pattern"); +const { ConnectionStringParser } = require("./utils"); class MySQLConnector { #connected = false; @@ -15,9 +15,9 @@ class MySQLConnector { } #parseDatabase() { - const connectionPattern = new UrlPattern("mysql\\://*@*/:database*"); - const match = connectionPattern.match(this.connectionString); - return match?.database; + const connectionParser = new ConnectionStringParser({ scheme: "mysql" }); + const parsed = connectionParser.parse(this.connectionString); + return parsed?.endpoint; } async connect() { diff --git a/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/utils.js b/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/utils.js new file mode 100644 index 00000000..c93c83a7 --- /dev/null +++ b/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/utils.js @@ -0,0 +1,182 @@ +// Credit: https://github.com/sindilevich/connection-string-parser + +/** + * @typedef {Object} ConnectionStringParserOptions + * @property {'mssql' | 'mysql' | 'postgresql' | 'db'} [scheme] - The scheme of the connection string + */ + +/** + * @typedef {Object} ConnectionStringObject + * @property {string} scheme - The scheme of the connection string eg: mongodb, mssql, mysql, postgresql, etc. + * @property {string} username - The username of the connection string + * @property {string} password - The password of the connection string + * @property {{host: string, port: number}[]} hosts - The hosts of the connection string + * @property {string} endpoint - The endpoint (database name) of the connection string + * @property {Object} options - The options of the connection string + */ +class ConnectionStringParser { + static DEFAULT_SCHEME = "db"; + + /** + * @param {ConnectionStringParserOptions} options + */ + constructor(options = {}) { + this.scheme = + (options && options.scheme) || ConnectionStringParser.DEFAULT_SCHEME; + } + + /** + * Takes a connection string object and returns a URI string of the form: + * + * scheme://[username[:password]@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[endpoint]][?options] + * @param {Object} connectionStringObject The object that describes connection string parameters + */ + format(connectionStringObject) { + if (!connectionStringObject) { + return this.scheme + "://localhost"; + } + if ( + this.scheme && + connectionStringObject.scheme && + this.scheme !== connectionStringObject.scheme + ) { + throw new Error(`Scheme not supported: ${connectionStringObject.scheme}`); + } + + let uri = + (this.scheme || + connectionStringObject.scheme || + ConnectionStringParser.DEFAULT_SCHEME) + "://"; + + if (connectionStringObject.username) { + uri += encodeURIComponent(connectionStringObject.username); + // Allow empty passwords + if (connectionStringObject.password) { + uri += ":" + encodeURIComponent(connectionStringObject.password); + } + uri += "@"; + } + uri += this._formatAddress(connectionStringObject); + // Only put a slash when there is an endpoint + if (connectionStringObject.endpoint) { + uri += "/" + encodeURIComponent(connectionStringObject.endpoint); + } + if ( + connectionStringObject.options && + Object.keys(connectionStringObject.options).length > 0 + ) { + uri += + "?" + + Object.keys(connectionStringObject.options) + .map( + (option) => + encodeURIComponent(option) + + "=" + + encodeURIComponent(connectionStringObject.options[option]) + ) + .join("&"); + } + return uri; + } + + /** + * Where scheme and hosts will always be present. Other fields will only be present in the result if they were + * present in the input. + * @param {string} uri The connection string URI + * @returns {ConnectionStringObject} The connection string object + */ + parse(uri) { + const connectionStringParser = new RegExp( + "^\\s*" + // Optional whitespace padding at the beginning of the line + "([^:]+)://" + // Scheme (Group 1) + "(?:([^:@,/?=&]+)(?::([^:@,/?=&]+))?@)?" + // User (Group 2) and Password (Group 3) + "([^@/?=&]+)" + // Host address(es) (Group 4) + "(?:/([^:@,/?=&]+)?)?" + // Endpoint (Group 5) + "(?:\\?([^:@,/?]+)?)?" + // Options (Group 6) + "\\s*$", // Optional whitespace padding at the end of the line + "gi" + ); + const connectionStringObject = {}; + + if (!uri.includes("://")) { + throw new Error(`No scheme found in URI ${uri}`); + } + + const tokens = connectionStringParser.exec(uri); + + if (Array.isArray(tokens)) { + connectionStringObject.scheme = tokens[1]; + if (this.scheme && this.scheme !== connectionStringObject.scheme) { + throw new Error(`URI must start with '${this.scheme}://'`); + } + connectionStringObject.username = tokens[2] + ? decodeURIComponent(tokens[2]) + : tokens[2]; + connectionStringObject.password = tokens[3] + ? decodeURIComponent(tokens[3]) + : tokens[3]; + connectionStringObject.hosts = this._parseAddress(tokens[4]); + connectionStringObject.endpoint = tokens[5] + ? decodeURIComponent(tokens[5]) + : tokens[5]; + connectionStringObject.options = tokens[6] + ? this._parseOptions(tokens[6]) + : tokens[6]; + } + return connectionStringObject; + } + + /** + * Formats the address portion of a connection string + * @param {Object} connectionStringObject The object that describes connection string parameters + */ + _formatAddress(connectionStringObject) { + return connectionStringObject.hosts + .map( + (address) => + encodeURIComponent(address.host) + + (address.port + ? ":" + encodeURIComponent(address.port.toString(10)) + : "") + ) + .join(","); + } + + /** + * Parses an address + * @param {string} addresses The address(es) to process + */ + _parseAddress(addresses) { + return addresses.split(",").map((address) => { + const i = address.indexOf(":"); + + return i >= 0 + ? { + host: decodeURIComponent(address.substring(0, i)), + port: +address.substring(i + 1), + } + : { host: decodeURIComponent(address) }; + }); + } + + /** + * Parses options + * @param {string} options The options to process + */ + _parseOptions(options) { + const result = {}; + + options.split("&").forEach((option) => { + const i = option.indexOf("="); + + if (i >= 0) { + result[decodeURIComponent(option.substring(0, i))] = decodeURIComponent( + option.substring(i + 1) + ); + } + }); + return result; + } +} + +module.exports = { ConnectionStringParser };