chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
const mssql = require("mssql");
|
||||
const { ConnectionStringParser } = require("./utils");
|
||||
|
||||
class MSSQLConnector {
|
||||
#connected = false;
|
||||
database_id = "";
|
||||
connectionConfig = {
|
||||
user: null,
|
||||
password: null,
|
||||
database: null,
|
||||
server: null,
|
||||
port: null,
|
||||
pool: {
|
||||
max: 10,
|
||||
min: 0,
|
||||
idleTimeoutMillis: 30000,
|
||||
},
|
||||
options: {
|
||||
encrypt: false,
|
||||
trustServerCertificate: true,
|
||||
},
|
||||
};
|
||||
|
||||
constructor(
|
||||
config = {
|
||||
// we will force into RFC-3986 from DB
|
||||
// eg: mssql://user:password@server:port/database?{...opts}
|
||||
connectionString: null, // we will force into RFC-3986
|
||||
}
|
||||
) {
|
||||
this.className = "MSSQLConnector";
|
||||
this.connectionString = config.connectionString;
|
||||
this._client = null;
|
||||
this.#parseDatabase();
|
||||
}
|
||||
|
||||
#parseDatabase() {
|
||||
const connectionParser = new ConnectionStringParser({ scheme: "mssql" });
|
||||
const parsed = connectionParser.parse(this.connectionString);
|
||||
|
||||
this.database_id = parsed?.endpoint;
|
||||
this.connectionConfig = {
|
||||
...this.connectionConfig,
|
||||
user: parsed?.username,
|
||||
password: parsed?.password,
|
||||
database: parsed?.endpoint,
|
||||
server: parsed?.hosts?.[0]?.host,
|
||||
port: parsed?.hosts?.[0]?.port,
|
||||
options: {
|
||||
...this.connectionConfig.options,
|
||||
encrypt: parsed?.options?.encrypt === "true",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async connect() {
|
||||
this._client = await mssql.connect(this.connectionConfig);
|
||||
this.#connected = true;
|
||||
return this._client;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} queryString the SQL query to be run
|
||||
* @param {Array} params optional parameters for prepared statement
|
||||
* @returns {Promise<import(".").QueryResult>}
|
||||
*/
|
||||
async runQuery(queryString = "", params = []) {
|
||||
const result = { rows: [], count: 0, error: null };
|
||||
try {
|
||||
if (!this.#connected) await this.connect();
|
||||
|
||||
const request = this._client.request();
|
||||
params.forEach((value, index) => {
|
||||
request.input(`p${index}`, value);
|
||||
});
|
||||
const query = await request.query(queryString);
|
||||
result.rows = query.recordset;
|
||||
result.count = query.rowsAffected.reduce((sum, a) => sum + a, 0);
|
||||
} catch (err) {
|
||||
console.log(this.className, err);
|
||||
result.error = err.message;
|
||||
} finally {
|
||||
// Check client is connected before closing since we use this for validation
|
||||
if (this._client) {
|
||||
await this._client.close();
|
||||
this.#connected = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async validateConnection() {
|
||||
try {
|
||||
const result = await this.runQuery("SELECT 1");
|
||||
return { success: !result.error, error: result.error };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
getTablesSql() {
|
||||
return `SELECT name FROM sysobjects WHERE xtype='U';`;
|
||||
}
|
||||
|
||||
getTableSchemaSql(table_name) {
|
||||
return {
|
||||
query: `SELECT COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @p0`,
|
||||
params: [table_name],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.MSSQLConnector = MSSQLConnector;
|
||||
@@ -0,0 +1,83 @@
|
||||
const mysql = require("mysql2/promise");
|
||||
const { ConnectionStringParser } = require("./utils");
|
||||
|
||||
class MySQLConnector {
|
||||
#connected = false;
|
||||
database_id = "";
|
||||
constructor(
|
||||
config = {
|
||||
connectionString: null,
|
||||
}
|
||||
) {
|
||||
this.className = "MySQLConnector";
|
||||
this.connectionString = config.connectionString;
|
||||
this._client = null;
|
||||
this.database_id = this.#parseDatabase();
|
||||
}
|
||||
|
||||
#parseDatabase() {
|
||||
const connectionParser = new ConnectionStringParser({ scheme: "mysql" });
|
||||
const parsed = connectionParser.parse(this.connectionString);
|
||||
return parsed?.endpoint;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
this._client = await mysql.createConnection({ uri: this.connectionString });
|
||||
this.#connected = true;
|
||||
return this._client;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} queryString the SQL query to be run
|
||||
* @param {Array} params optional parameters for prepared statement
|
||||
* @returns {Promise<import(".").QueryResult>}
|
||||
*/
|
||||
async runQuery(queryString = "", params = []) {
|
||||
const result = { rows: [], count: 0, error: null };
|
||||
try {
|
||||
if (!this.#connected) await this.connect();
|
||||
const [query] =
|
||||
params.length > 0
|
||||
? await this._client.execute(queryString, params)
|
||||
: await this._client.query(queryString);
|
||||
result.rows = query;
|
||||
result.count = query?.length;
|
||||
} catch (err) {
|
||||
console.log(this.className, err);
|
||||
result.error = err.message;
|
||||
} finally {
|
||||
// Check client is connected before closing since we use this for validation
|
||||
if (this._client) {
|
||||
await this._client.end();
|
||||
this.#connected = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async validateConnection() {
|
||||
try {
|
||||
const result = await this.runQuery("SELECT 1");
|
||||
return { success: !result.error, error: result.error };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
getTablesSql() {
|
||||
return {
|
||||
query: `SELECT table_name FROM information_schema.tables WHERE table_schema = ?`,
|
||||
params: [this.database_id],
|
||||
};
|
||||
}
|
||||
|
||||
getTableSchemaSql(table_name) {
|
||||
return {
|
||||
query: `SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT, EXTRA FROM information_schema.columns WHERE table_schema = ? AND table_name = ?`,
|
||||
params: [this.database_id, table_name],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.MySQLConnector = MySQLConnector;
|
||||
@@ -0,0 +1,75 @@
|
||||
const pgSql = require("pg");
|
||||
|
||||
class PostgresSQLConnector {
|
||||
#connected = false;
|
||||
constructor(
|
||||
config = {
|
||||
connectionString: null,
|
||||
schema: null,
|
||||
}
|
||||
) {
|
||||
this.className = "PostgresSQLConnector";
|
||||
this.connectionString = config.connectionString;
|
||||
this.schema = config.schema || "public";
|
||||
this._client = new pgSql.Client({
|
||||
connectionString: this.connectionString,
|
||||
});
|
||||
}
|
||||
|
||||
async connect() {
|
||||
await this._client.connect();
|
||||
this.#connected = true;
|
||||
return this._client;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} queryString the SQL query to be run
|
||||
* @param {Array} params optional parameters for prepared statement
|
||||
* @returns {Promise<import(".").QueryResult>}
|
||||
*/
|
||||
async runQuery(queryString = "", params = []) {
|
||||
const result = { rows: [], count: 0, error: null };
|
||||
try {
|
||||
if (!this.#connected) await this.connect();
|
||||
const query = await this._client.query(queryString, params);
|
||||
result.rows = query.rows;
|
||||
result.count = query.rowCount;
|
||||
} catch (err) {
|
||||
console.log(this.className, err);
|
||||
result.error = err.message;
|
||||
} finally {
|
||||
// Check client is connected before closing since we use this for validation
|
||||
if (this._client) {
|
||||
await this._client.end();
|
||||
this.#connected = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async validateConnection() {
|
||||
try {
|
||||
const result = await this.runQuery("SELECT 1");
|
||||
return { success: !result.error, error: result.error };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
getTablesSql() {
|
||||
return {
|
||||
query: `SELECT * FROM pg_catalog.pg_tables WHERE schemaname = $1`,
|
||||
params: [this.schema],
|
||||
};
|
||||
}
|
||||
|
||||
getTableSchemaSql(table_name) {
|
||||
return {
|
||||
query: `SELECT column_name, data_type, character_maximum_length, column_default, is_nullable FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = $1 AND table_schema = $2`,
|
||||
params: [table_name, this.schema],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.PostgresSQLConnector = PostgresSQLConnector;
|
||||
@@ -0,0 +1,80 @@
|
||||
const { SystemSettings } = require("../../../../../../models/systemSettings");
|
||||
const { safeJsonParse } = require("../../../../../http");
|
||||
|
||||
/**
|
||||
* @typedef {('postgresql'|'mysql'|'sql-server')} SQLEngine
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} QueryResult
|
||||
* @property {[number]} rows - The query result rows
|
||||
* @property {number} count - Number of rows the query returned/changed
|
||||
* @property {string|null} error - Error string if there was an issue
|
||||
*/
|
||||
|
||||
/**
|
||||
* A valid database SQL connection object
|
||||
* @typedef {Object} SQLConnection
|
||||
* @property {string} database_id - Unique identifier of the database connection
|
||||
* @property {SQLEngine} engine - Engine used by connection
|
||||
* @property {string} connectionString - RFC connection string for db
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {SQLEngine} identifier
|
||||
* @param {object} connectionConfig
|
||||
* @returns Database Connection Engine Class for SQLAgent or throws error
|
||||
*/
|
||||
function getDBClient(identifier = "", connectionConfig = {}) {
|
||||
switch (identifier) {
|
||||
case "mysql":
|
||||
const { MySQLConnector } = require("./MySQL");
|
||||
return new MySQLConnector(connectionConfig);
|
||||
case "postgresql":
|
||||
const { PostgresSQLConnector } = require("./Postgresql");
|
||||
return new PostgresSQLConnector(connectionConfig);
|
||||
case "sql-server":
|
||||
const { MSSQLConnector } = require("./MSSQL");
|
||||
return new MSSQLConnector(connectionConfig);
|
||||
default:
|
||||
throw new Error(
|
||||
`There is no supported database connector for ${identifier}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all of the known database connection that can be used by the agent.
|
||||
* @returns {Promise<[SQLConnection]>}
|
||||
*/
|
||||
async function listSQLConnections() {
|
||||
return safeJsonParse(
|
||||
(await SystemSettings.get({ label: "agent_sql_connections" }))?.value,
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a SQL connection by attempting to connect and run a simple query
|
||||
* @param {SQLEngine} identifier - The SQL engine type
|
||||
* @param {object} connectionConfig - The connection configuration
|
||||
* @returns {Promise<{success: boolean, error: string|null}>}
|
||||
*/
|
||||
async function validateConnection(identifier = "", connectionConfig = {}) {
|
||||
try {
|
||||
const client = getDBClient(identifier, connectionConfig);
|
||||
return await client.validateConnection();
|
||||
} catch {
|
||||
console.log(`Failed to connect to ${identifier} database.`);
|
||||
return {
|
||||
success: false,
|
||||
error: `Unable to connect to ${identifier}. Please verify your connection details.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getDBClient,
|
||||
listSQLConnections,
|
||||
validateConnection,
|
||||
};
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user