import type { ConnectionConfig, DatabaseType } from "@/types/database"; export interface ParsedConnectionUrl { dbType: DatabaseType; driverProfile: string; driverLabel: string; host: string; port: number; username: string; password: string; database?: string; urlParams: string; ssl: boolean; connectionString?: string; oracleConnectionType?: "service_name" | "sid"; useMongoUrl?: boolean; } export type ConnectionProfile = { type: DatabaseType; profile: string; label: string; defaultPort: number; }; const SCHEME_PROFILES: Record = { mysql: { type: "mysql", profile: "mysql", label: "MySQL", defaultPort: 3306 }, mariadb: { type: "mysql", profile: "mariadb", label: "MariaDB", defaultPort: 3306 }, postgres: { type: "postgres", profile: "postgres", label: "PostgreSQL", defaultPort: 5432 }, postgresql: { type: "postgres", profile: "postgres", label: "PostgreSQL", defaultPort: 5432 }, redshift: { type: "redshift", profile: "redshift", label: "Redshift", defaultPort: 5439 }, redis: { type: "redis", profile: "redis", label: "Redis", defaultPort: 6379 }, rediss: { type: "redis", profile: "redis", label: "Redis", defaultPort: 6379 }, mongodb: { type: "mongodb", profile: "mongodb", label: "MongoDB", defaultPort: 27017 }, "mongodb+srv": { type: "mongodb", profile: "mongodb", label: "MongoDB", defaultPort: 27017 }, clickhouse: { type: "clickhouse", profile: "clickhouse", label: "ClickHouse", defaultPort: 8123 }, sqlserver: { type: "sqlserver", profile: "sqlserver", label: "SQL Server", defaultPort: 1433 }, mssql: { type: "sqlserver", profile: "sqlserver", label: "SQL Server", defaultPort: 1433 }, oracle: { type: "oracle", profile: "oracle", label: "Oracle", defaultPort: 1521 }, elasticsearch: { type: "elasticsearch", profile: "elasticsearch", label: "Elasticsearch", defaultPort: 9200 }, dm: { type: "dameng", profile: "dm", label: "DM (Dameng)", defaultPort: 5236 }, dameng: { type: "dameng", profile: "dm", label: "DM (Dameng)", defaultPort: 5236 }, gaussdb: { type: "gaussdb", profile: "gaussdb", label: "GaussDB", defaultPort: 5432 }, yashandb: { type: "yashandb", profile: "yashandb", label: "YashanDB", defaultPort: 1688 }, opengauss: { type: "gaussdb", profile: "opengauss", label: "openGauss", defaultPort: 5432 }, tdengine: { type: "tdengine", profile: "tdengine", label: "TDengine", defaultPort: 6041 }, "taos-ws": { type: "tdengine", profile: "tdengine", label: "TDengine", defaultPort: 6041 }, xugu: { type: "xugu", profile: "xugu", label: "XuguDB", defaultPort: 5138 }, iris: { type: "iris", profile: "iris", label: "IRIS", defaultPort: 1972 }, }; const HTTP_SELECTED_PROFILES: Record = { clickhouse: SCHEME_PROFILES.clickhouse, elasticsearch: SCHEME_PROFILES.elasticsearch, }; function decodeUrlPart(value: string): string { try { return decodeURIComponent(value); } catch { return value; } } function databaseFromPath(pathname: string): string | undefined { const value = pathname.replace(/^\/+/, ""); if (!value) return undefined; return decodeUrlPart(value.split("/")[0]); } function queryParamValue(params: string, key: string): string | undefined { for (const part of params.split(/[&;]/)) { if (!part) continue; const [rawKey, ...rest] = part.split("="); if (decodeUrlPart(rawKey).toLowerCase() === key.toLowerCase()) { return decodeUrlPart(rest.join("=")).trim(); } } return undefined; } function extractMysqlCredentialParams(params: string): { username?: string; password?: string; urlParams: string } { let username: string | undefined; let password: string | undefined; let foundCredentialParam = false; const urlParams: string[] = []; for (const part of params.split(/[&;]/)) { if (!part) continue; const [rawKey, ...rest] = part.split("="); const key = decodeUrlPart(rawKey).trim().toLowerCase(); if (key === "user") { username = decodeUrlPart(rest.join("=")).trim(); foundCredentialParam = true; } else if (key === "password") { password = decodeUrlPart(rest.join("=")).trim(); foundCredentialParam = true; } else { urlParams.push(part); } } return { username, password, urlParams: foundCredentialParam ? urlParams.join("&") : params }; } function urlParamsRequireTls(dbType: DatabaseType, params: string): boolean { if (dbType === "mysql") { const requireSsl = queryParamValue(params, "require_ssl")?.toLowerCase(); if (requireSsl === "true" || requireSsl === "1" || requireSsl === "yes") return true; const sslMode = (queryParamValue(params, "ssl-mode") || queryParamValue(params, "sslmode") || "") .toLowerCase() .replace("-", "_"); return sslMode === "required" || sslMode === "require" || sslMode === "verify_ca" || sslMode === "verify_identity"; } if (dbType === "postgres" || dbType === "redshift") { const sslMode = (queryParamValue(params, "sslmode") || "").toLowerCase(); return sslMode === "require" || sslMode === "verify-ca" || sslMode === "verify-full"; } return false; } export function connectionProfileForScheme(scheme: string, preferredProfile?: string): ConnectionProfile | undefined { if ((scheme === "http" || scheme === "https") && preferredProfile) { return HTTP_SELECTED_PROFILES[preferredProfile]; } return SCHEME_PROFILES[scheme]; } function parseJdbcSqlServerUrl(source: string): ParsedConnectionUrl | null { const match = source.match(/^jdbc:sqlserver:\/\/([^;:/]+)(?::(\d+))?(?:;(.*))?$/i); if (!match) return null; const profile = SCHEME_PROFILES.sqlserver; const props = new Map(); const urlParams: string[] = []; for (const part of (match[3] || "").split(";")) { if (!part) continue; const [rawKey, ...rest] = part.split("="); const key = rawKey.trim(); const value = rest.join("="); const normalizedKey = key.toLowerCase(); if (normalizedKey === "databasename" || normalizedKey === "database" || normalizedKey === "user") { props.set(normalizedKey, value); } else if (normalizedKey === "password") { props.set(normalizedKey, value); } else { urlParams.push(part); } } return { dbType: profile.type, driverProfile: profile.profile, driverLabel: profile.label, host: match[1], port: match[2] ? Number(match[2]) : profile.defaultPort, username: decodeUrlPart(props.get("user") || ""), password: decodeUrlPart(props.get("password") || ""), database: decodeUrlPart(props.get("databasename") || props.get("database") || "") || undefined, urlParams: urlParams.join(";"), ssl: false, }; } function parseJdbcOracleUrl(source: string): ParsedConnectionUrl | null { const descriptorMatch = source.match(/^jdbc:oracle:thin:@\s*\((.+)\)\s*$/i); if (descriptorMatch) { const profile = SCHEME_PROFILES.oracle; const host = oracleDescriptorValue(source, "HOST"); const port = oracleDescriptorValue(source, "PORT"); const serviceName = oracleDescriptorValue(source, "SERVICE_NAME"); const sid = oracleDescriptorValue(source, "SID"); if (!host) return null; return { dbType: profile.type, driverProfile: profile.profile, driverLabel: profile.label, host, port: port ? Number(port) : profile.defaultPort, username: "", password: "", database: serviceName || sid || undefined, urlParams: "", ssl: false, connectionString: source, oracleConnectionType: sid && !serviceName ? "sid" : "service_name", }; } const serviceMatch = source.match(/^jdbc:oracle:thin:@\/\/([^:/?#]+)(?::(\d+))?\/([^?]+)(?:\?(.*))?$/i); if (serviceMatch) { const profile = SCHEME_PROFILES.oracle; return { dbType: profile.type, driverProfile: profile.profile, driverLabel: profile.label, host: serviceMatch[1], port: serviceMatch[2] ? Number(serviceMatch[2]) : profile.defaultPort, username: "", password: "", database: decodeUrlPart(serviceMatch[3]), urlParams: serviceMatch[4] || "", ssl: false, oracleConnectionType: "service_name", }; } const sidMatch = source.match(/^jdbc:oracle:thin:@([^:/?#]+)(?::(\d+))?:([^?]+)(?:\?(.*))?$/i); if (sidMatch) { const profile = SCHEME_PROFILES.oracle; return { dbType: profile.type, driverProfile: profile.profile, driverLabel: profile.label, host: sidMatch[1], port: sidMatch[2] ? Number(sidMatch[2]) : profile.defaultPort, username: "", password: "", database: decodeUrlPart(sidMatch[3]), urlParams: sidMatch[4] || "", ssl: false, oracleConnectionType: "sid", }; } return null; } function oracleDescriptorValue(source: string, key: string): string | undefined { const match = new RegExp(`\\(${key}\\s*=\\s*([^\\)]+)\\)`, "i").exec(source); return match?.[1]?.trim(); } function parseJdbcUCanAccessUrl(source: string): ParsedConnectionUrl | null { const match = source.match(/^jdbc:ucanaccess:\/\/(.+?)(?:;.*)?$/i); if (!match) return null; const filePath = decodeUrlPart(match[1]); const normalizedPath = filePath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(filePath) ? filePath : `/${filePath}`; const database = normalizedPath.split(/[\\/]/).filter(Boolean).pop(); return { dbType: "access", driverProfile: "access", driverLabel: "Microsoft Access", host: normalizedPath, port: 0, username: "", password: "", database, urlParams: "", ssl: false, connectionString: source, }; } export function parseConnectionUrl(value: string, preferredProfile?: string): ParsedConnectionUrl { const input = value.trim(); if (!input) { throw new Error("Connection URL is empty"); } const jdbcUCanAccess = parseJdbcUCanAccessUrl(input); if (jdbcUCanAccess) return jdbcUCanAccess; const jdbcOracle = parseJdbcOracleUrl(input); if (jdbcOracle) return jdbcOracle; const jdbcSqlServer = parseJdbcSqlServerUrl(input); if (jdbcSqlServer) return jdbcSqlServer; const isJdbcUrl = /^jdbc:/i.test(input); const source = isJdbcUrl ? input.replace(/^jdbc:/i, "") : input; let parsed: URL; try { parsed = new URL(source); } catch { throw new Error("Invalid connection URL"); } const scheme = parsed.protocol.replace(/:$/, "").toLowerCase(); const profile = connectionProfileForScheme(scheme, preferredProfile); if (!profile) { throw new Error(`Unsupported connection URL scheme: ${scheme}`); } const urlParams = parsed.search.replace(/^\?/, ""); const normalizedFragment = decodeUrlPart(parsed.hash.replace(/^#/, "")).trim().toLowerCase(); const parsedUrlParams = profile.type === "redis" && normalizedFragment === "insecure" ? [urlParams, "insecure=true"].filter(Boolean).join("&") : urlParams; const mysqlCredentials = isJdbcUrl && profile.type === "mysql" ? extractMysqlCredentialParams(parsedUrlParams) : undefined; const effectiveUrlParams = mysqlCredentials?.urlParams ?? parsedUrlParams; if (profile.type === "mongodb") { return { dbType: profile.type, driverProfile: profile.profile, driverLabel: profile.label, host: parsed.hostname, port: parsed.port ? Number(parsed.port) : profile.defaultPort, username: decodeUrlPart(parsed.username), password: decodeUrlPart(parsed.password), database: databaseFromPath(parsed.pathname), urlParams: parsedUrlParams, ssl: scheme === "mongodb+srv", connectionString: source, useMongoUrl: true, }; } return { dbType: profile.type, driverProfile: profile.profile, driverLabel: profile.label, host: parsed.hostname, port: parsed.port ? Number(parsed.port) : profile.defaultPort, username: mysqlCredentials?.username ?? decodeUrlPart(parsed.username), password: mysqlCredentials?.password ?? decodeUrlPart(parsed.password), database: databaseFromPath(parsed.pathname), urlParams: effectiveUrlParams, ssl: scheme === "rediss" || scheme === "https" || urlParamsRequireTls(profile.type, effectiveUrlParams), }; } export function applyParsedConnectionUrl( config: Omit, parsed: ParsedConnectionUrl, ): Omit { return { ...config, db_type: parsed.dbType, driver_profile: parsed.driverProfile, driver_label: parsed.driverLabel, host: parsed.host, port: parsed.port, username: parsed.username, password: parsed.password, database: parsed.database, url_params: parsed.urlParams, ssl: parsed.ssl, connection_string: parsed.connectionString, oracle_connection_type: parsed.oracleConnectionType, }; }