@bytebase/dbhub
Version:
Token-efficient database MCP server for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite
544 lines (539 loc) • 15.6 kB
JavaScript
// src/connectors/interface.ts
var _ConnectorRegistry = class _ConnectorRegistry {
/**
* Register a new connector
*/
static register(connector) {
_ConnectorRegistry.connectors.set(connector.id, connector);
}
/**
* Get a connector by ID
*/
static getConnector(id) {
return _ConnectorRegistry.connectors.get(id) || null;
}
/**
* Get connector for a DSN string
* Tries to find a connector that can handle the given DSN format
*/
static getConnectorForDSN(dsn) {
for (const connector of _ConnectorRegistry.connectors.values()) {
if (connector.dsnParser.isValidDSN(dsn)) {
return connector;
}
}
return null;
}
/**
* Get all available connector IDs
*/
static getAvailableConnectors() {
return Array.from(_ConnectorRegistry.connectors.keys());
}
/**
* Get sample DSN for a specific connector
*/
static getSampleDSN(connectorType) {
const connector = _ConnectorRegistry.getConnector(connectorType);
if (!connector) return null;
return connector.dsnParser.getSampleDSN();
}
/**
* Get all available sample DSNs
*/
static getAllSampleDSNs() {
const samples = {};
for (const [id, connector] of _ConnectorRegistry.connectors.entries()) {
samples[id] = connector.dsnParser.getSampleDSN();
}
return samples;
}
};
_ConnectorRegistry.connectors = /* @__PURE__ */ new Map();
var ConnectorRegistry = _ConnectorRegistry;
// src/utils/safe-url.ts
var SafeURL = class {
/**
* Parse a URL and handle special characters in passwords
* This is a safe alternative to the URL constructor
*
* @param urlString - The DSN string to parse
*/
constructor(urlString) {
this.protocol = "";
this.hostname = "";
this.port = "";
this.pathname = "";
this.username = "";
this.password = "";
this.searchParams = /* @__PURE__ */ new Map();
if (!urlString || urlString.trim() === "") {
throw new Error("URL string cannot be empty");
}
try {
const protocolSeparator = urlString.indexOf("://");
if (protocolSeparator !== -1) {
this.protocol = urlString.substring(0, protocolSeparator + 1);
urlString = urlString.substring(protocolSeparator + 3);
} else {
throw new Error('Invalid URL format: missing protocol (e.g., "mysql://")');
}
const questionMarkIndex = urlString.indexOf("?");
let queryParams = "";
if (questionMarkIndex !== -1) {
queryParams = urlString.substring(questionMarkIndex + 1);
urlString = urlString.substring(0, questionMarkIndex);
queryParams.split("&").forEach((pair) => {
const parts = pair.split("=");
if (parts.length === 2 && parts[0] && parts[1]) {
this.searchParams.set(parts[0], decodeURIComponent(parts[1]));
}
});
}
const pathStart = urlString.indexOf("/");
const authorityEnd = pathStart === -1 ? urlString.length : pathStart;
const atIndex = urlString.lastIndexOf("@", authorityEnd - 1);
if (atIndex !== -1) {
const auth = urlString.substring(0, atIndex);
urlString = urlString.substring(atIndex + 1);
const colonIndex2 = auth.indexOf(":");
if (colonIndex2 !== -1) {
this.username = auth.substring(0, colonIndex2);
this.password = auth.substring(colonIndex2 + 1);
this.username = decodeURIComponent(this.username);
this.password = decodeURIComponent(this.password);
} else {
this.username = auth;
}
}
const pathSeparatorIndex = urlString.indexOf("/");
if (pathSeparatorIndex !== -1) {
this.pathname = urlString.substring(pathSeparatorIndex);
urlString = urlString.substring(0, pathSeparatorIndex);
}
const colonIndex = urlString.indexOf(":");
if (colonIndex !== -1) {
this.hostname = urlString.substring(0, colonIndex);
this.port = urlString.substring(colonIndex + 1);
} else {
this.hostname = urlString;
}
if (this.protocol === "") {
throw new Error("Invalid URL: protocol is required");
}
} catch (error) {
throw new Error(`Failed to parse URL: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Helper method to safely get a parameter from query string
*
* @param name - The parameter name to retrieve
* @returns The parameter value or null if not found
*/
getSearchParam(name) {
return this.searchParams.has(name) ? this.searchParams.get(name) : null;
}
/**
* Helper method to iterate over all parameters
*
* @param callback - Function to call for each parameter
*/
forEachSearchParam(callback) {
this.searchParams.forEach((value, key) => callback(value, key));
}
};
// src/utils/dsn-obfuscate.ts
function parseConnectionInfoFromDSN(dsn) {
if (!dsn) {
return null;
}
try {
const type = getDatabaseTypeFromDSN(dsn);
if (typeof type === "undefined") {
return null;
}
if (type === "sqlite") {
const prefix = "sqlite:///";
if (dsn.length > prefix.length) {
const rawPath = dsn.substring(prefix.length);
const firstChar = rawPath[0];
const isWindowsDrive = rawPath.length > 1 && rawPath[1] === ":";
const isSpecialPath = firstChar === ":" || firstChar === "." || firstChar === "~" || isWindowsDrive;
return {
type,
database: isSpecialPath ? rawPath : "/" + rawPath
};
}
return { type };
}
const url = new SafeURL(dsn);
const info = { type };
if (url.hostname) {
info.host = url.hostname;
}
if (url.port) {
info.port = parseInt(url.port, 10);
}
if (url.pathname && url.pathname.length > 1) {
info.database = url.pathname.substring(1);
}
if (url.username) {
info.user = url.username;
}
return info;
} catch {
return null;
}
}
var REDACTED_DSN = "<redacted DSN>";
function obfuscateDSNPassword(dsn) {
if (!dsn) {
return dsn;
}
try {
const type = getDatabaseTypeFromDSN(dsn);
if (type === "sqlite") {
return dsn;
}
const url = new SafeURL(dsn);
if (url.port && !/^\d+$/.test(url.port)) {
return REDACTED_DSN;
}
if (!url.password) {
return dsn;
}
const obfuscatedPassword = "*".repeat(Math.min(url.password.length, 8));
const protocol = dsn.split(":")[0];
let result;
if (url.username) {
result = `${protocol}://${url.username}:${obfuscatedPassword}@${url.hostname}`;
} else {
result = `${protocol}://${obfuscatedPassword}@${url.hostname}`;
}
if (url.port) {
result += `:${url.port}`;
}
result += url.pathname;
if (url.searchParams.size > 0) {
const params = [];
url.forEachSearchParam((value, key) => {
params.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
});
result += `?${params.join("&")}`;
}
return result;
} catch {
return REDACTED_DSN;
}
}
function getDatabaseTypeFromDSN(dsn) {
if (!dsn) {
return void 0;
}
const protocol = dsn.split(":")[0];
return protocolToConnectorType(protocol);
}
function protocolToConnectorType(protocol) {
const mapping = {
"postgres": "postgres",
"postgresql": "postgres",
"mysql": "mysql",
"mariadb": "mariadb",
"sqlserver": "sqlserver",
"sqlite": "sqlite",
"oracle": "oracle"
};
return mapping[protocol];
}
function getDefaultPortForType(type) {
const ports = {
"postgres": 5432,
"mysql": 3306,
"mariadb": 3306,
"sqlserver": 1433,
"oracle": 1521,
"sqlite": void 0
};
return ports[type];
}
// src/utils/sql-parser.ts
var TokenType = { Plain: 0, Comment: 1, QuotedBlock: 2 };
function plainToken(i) {
return { type: TokenType.Plain, end: i + 1 };
}
function scanSingleLineComment(sql, i) {
if (sql[i] !== "-" || sql[i + 1] !== "-") {
return null;
}
let j = i;
while (j < sql.length && sql[j] !== "\n") {
j++;
}
return { type: TokenType.Comment, end: j };
}
function scanSingleLineCommentMySQL(sql, i) {
if (sql[i] !== "-" || sql[i + 1] !== "-") {
return null;
}
const next = sql[i + 2];
if (next !== void 0 && next.charCodeAt(0) > 32 && next.charCodeAt(0) !== 127) {
return null;
}
let j = i;
while (j < sql.length && sql[j] !== "\n") {
j++;
}
return { type: TokenType.Comment, end: j };
}
function scanMultiLineComment(sql, i) {
if (sql[i] !== "/" || sql[i + 1] !== "*") {
return null;
}
let j = i + 2;
while (j < sql.length && !(sql[j] === "*" && sql[j + 1] === "/")) {
j++;
}
if (j < sql.length) {
j += 2;
}
return { type: TokenType.Comment, end: j };
}
function scanMultiLineCommentMySQL(sql, i) {
if (sql[i] !== "/" || sql[i + 1] !== "*") {
return null;
}
const next = sql[i + 2];
const nextNext = sql[i + 3];
if (next === "!" || next === "M" && nextNext === "!") {
return null;
}
return scanMultiLineComment(sql, i);
}
function scanNestedMultiLineComment(sql, i) {
if (sql[i] !== "/" || sql[i + 1] !== "*") {
return null;
}
let j = i + 2;
let depth = 1;
while (j < sql.length && depth > 0) {
if (sql[j] === "/" && sql[j + 1] === "*") {
depth++;
j += 2;
} else if (sql[j] === "*" && sql[j + 1] === "/") {
depth--;
j += 2;
} else {
j++;
}
}
return { type: TokenType.Comment, end: j };
}
function scanSingleQuotedString(sql, i) {
if (sql[i] !== "'") {
return null;
}
let j = i + 1;
while (j < sql.length) {
if (sql[j] === "'" && sql[j + 1] === "'") {
j += 2;
} else if (sql[j] === "'") {
j++;
break;
} else {
j++;
}
}
return { type: TokenType.QuotedBlock, end: j };
}
function scanDoubleQuotedString(sql, i) {
if (sql[i] !== '"') {
return null;
}
let j = i + 1;
while (j < sql.length) {
if (sql[j] === '"' && sql[j + 1] === '"') {
j += 2;
} else if (sql[j] === '"') {
j++;
break;
} else {
j++;
}
}
return { type: TokenType.QuotedBlock, end: j };
}
var dollarQuoteOpenRegex = /^\$([a-zA-Z_]\w*)?\$/;
function scanDollarQuotedBlock(sql, i) {
if (sql[i] !== "$") {
return null;
}
const next = sql[i + 1];
if (next >= "0" && next <= "9") {
return null;
}
const remaining = sql.substring(i);
const m = dollarQuoteOpenRegex.exec(remaining);
if (!m) {
return null;
}
const tag = m[0];
const bodyStart = i + tag.length;
const closeIdx = sql.indexOf(tag, bodyStart);
const end = closeIdx !== -1 ? closeIdx + tag.length : sql.length;
return { type: TokenType.QuotedBlock, end };
}
function scanBacktickQuotedIdentifier(sql, i) {
if (sql[i] !== "`") {
return null;
}
let j = i + 1;
while (j < sql.length) {
if (sql[j] === "`" && sql[j + 1] === "`") {
j += 2;
} else if (sql[j] === "`") {
j++;
break;
} else {
j++;
}
}
return { type: TokenType.QuotedBlock, end: j };
}
function scanBracketQuotedIdentifier(sql, i) {
if (sql[i] !== "[") {
return null;
}
let j = i + 1;
while (j < sql.length) {
if (sql[j] === "]" && sql[j + 1] === "]") {
j += 2;
} else if (sql[j] === "]") {
j++;
break;
} else {
j++;
}
}
return { type: TokenType.QuotedBlock, end: j };
}
function scanTokenAnsi(sql, i) {
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? plainToken(i);
}
function scanTokenPostgres(sql, i) {
return scanSingleLineComment(sql, i) ?? scanNestedMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanDollarQuotedBlock(sql, i) ?? plainToken(i);
}
function scanTokenMySQL(sql, i) {
return scanSingleLineCommentMySQL(sql, i) ?? scanMultiLineCommentMySQL(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? plainToken(i);
}
function scanTokenSQLite(sql, i) {
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? scanBracketQuotedIdentifier(sql, i) ?? plainToken(i);
}
function scanTokenSQLServer(sql, i) {
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBracketQuotedIdentifier(sql, i) ?? plainToken(i);
}
function scanOracleAlternativeQuotedString(sql, i) {
if (sql[i] !== "q" && sql[i] !== "Q" || sql[i + 1] !== "'") {
return null;
}
const open = sql[i + 2];
if (open === void 0 || open === " " || open === " " || open === "\n") {
return null;
}
const mirrors = { "(": ")", "[": "]", "{": "}", "<": ">" };
const close = (mirrors[open] ?? open) + "'";
const closeIdx = sql.indexOf(close, i + 3);
const end = closeIdx !== -1 ? closeIdx + close.length : sql.length;
return { type: TokenType.QuotedBlock, end };
}
function scanTokenOracle(sql, i) {
return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanOracleAlternativeQuotedString(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? plainToken(i);
}
var dialectScanners = {
postgres: scanTokenPostgres,
mysql: scanTokenMySQL,
mariadb: scanTokenMySQL,
sqlite: scanTokenSQLite,
sqlserver: scanTokenSQLServer,
oracle: scanTokenOracle
};
function getScanner(dialect) {
return dialect ? dialectScanners[dialect] ?? scanTokenAnsi : scanTokenAnsi;
}
function stripCommentsAndStrings(sql, dialect) {
const scanToken = getScanner(dialect);
const parts = [];
let plainStart = -1;
let i = 0;
while (i < sql.length) {
const token = scanToken(sql, i);
if (token.type === TokenType.Plain) {
if (plainStart === -1) {
plainStart = i;
}
} else {
if (plainStart !== -1) {
parts.push(sql.substring(plainStart, i));
plainStart = -1;
}
parts.push(" ");
}
i = token.end;
}
if (plainStart !== -1) {
parts.push(sql.substring(plainStart));
}
return parts.join("");
}
function blankCommentsAndStrings(sql, dialect) {
const scanToken = getScanner(dialect);
let result = "";
let i = 0;
while (i < sql.length) {
const token = scanToken(sql, i);
if (token.type === TokenType.Plain) {
result += sql[i];
} else {
result += " ".repeat(token.end - i);
}
i = token.end;
}
return result;
}
var LEADING_SQL_NOISE = /^(?:\s+|--[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*/;
function splitSQLStatements(sql, dialect) {
const scanToken = getScanner(dialect);
const statements = [];
let stmtStart = 0;
let i = 0;
while (i < sql.length) {
if (sql[i] === ";") {
const trimmed2 = sql.substring(stmtStart, i).trim();
if (trimmed2.length > 0) {
statements.push(trimmed2);
}
stmtStart = i + 1;
i++;
continue;
}
const token = scanToken(sql, i);
i = token.end;
}
const trimmed = sql.substring(stmtStart).trim();
if (trimmed.length > 0) {
statements.push(trimmed);
}
return statements;
}
export {
ConnectorRegistry,
SafeURL,
parseConnectionInfoFromDSN,
obfuscateDSNPassword,
getDatabaseTypeFromDSN,
getDefaultPortForType,
stripCommentsAndStrings,
blankCommentsAndStrings,
LEADING_SQL_NOISE,
splitSQLStatements
};