UNPKG

@bytebase/dbhub

Version:

Token-efficient database MCP server for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite

544 lines (539 loc) • 15.6 kB
// 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 };