@bytebase/dbhub 0.19.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,12 @@
1
+ import {
2
+ ConnectorRegistry,
3
+ SafeURL,
4
+ getDatabaseTypeFromDSN,
5
+ getDefaultPortForType,
6
+ parseConnectionInfoFromDSN,
7
+ stripCommentsAndStrings
8
+ } from "./chunk-C7WEAPX4.js";
9
+
1
10
  // src/tools/builtin-tools.ts
2
11
  var BUILTIN_TOOL_EXECUTE_SQL = "execute_sql";
3
12
  var BUILTIN_TOOL_SEARCH_OBJECTS = "search_objects";
@@ -6,60 +15,6 @@ var BUILTIN_TOOLS = [
6
15
  BUILTIN_TOOL_SEARCH_OBJECTS
7
16
  ];
8
17
 
9
- // src/connectors/interface.ts
10
- var _ConnectorRegistry = class _ConnectorRegistry {
11
- /**
12
- * Register a new connector
13
- */
14
- static register(connector) {
15
- _ConnectorRegistry.connectors.set(connector.id, connector);
16
- }
17
- /**
18
- * Get a connector by ID
19
- */
20
- static getConnector(id) {
21
- return _ConnectorRegistry.connectors.get(id) || null;
22
- }
23
- /**
24
- * Get connector for a DSN string
25
- * Tries to find a connector that can handle the given DSN format
26
- */
27
- static getConnectorForDSN(dsn) {
28
- for (const connector of _ConnectorRegistry.connectors.values()) {
29
- if (connector.dsnParser.isValidDSN(dsn)) {
30
- return connector;
31
- }
32
- }
33
- return null;
34
- }
35
- /**
36
- * Get all available connector IDs
37
- */
38
- static getAvailableConnectors() {
39
- return Array.from(_ConnectorRegistry.connectors.keys());
40
- }
41
- /**
42
- * Get sample DSN for a specific connector
43
- */
44
- static getSampleDSN(connectorType) {
45
- const connector = _ConnectorRegistry.getConnector(connectorType);
46
- if (!connector) return null;
47
- return connector.dsnParser.getSampleDSN();
48
- }
49
- /**
50
- * Get all available sample DSNs
51
- */
52
- static getAllSampleDSNs() {
53
- const samples = {};
54
- for (const [id, connector] of _ConnectorRegistry.connectors.entries()) {
55
- samples[id] = connector.dsnParser.getSampleDSN();
56
- }
57
- return samples;
58
- }
59
- };
60
- _ConnectorRegistry.connectors = /* @__PURE__ */ new Map();
61
- var ConnectorRegistry = _ConnectorRegistry;
62
-
63
18
  // src/utils/ssh-tunnel.ts
64
19
  import { Client } from "ssh2";
65
20
  import { readFileSync as readFileSync2 } from "fs";
@@ -263,8 +218,21 @@ var SSHTunnel = class {
263
218
  try {
264
219
  const resolvedKeyPath = resolveSymlink(config.privateKey);
265
220
  privateKeyBuffer = readFileSync2(resolvedKeyPath);
266
- } catch (error) {
267
- throw new Error(`Failed to read private key file: ${error instanceof Error ? error.message : String(error)}`);
221
+ } catch {
222
+ try {
223
+ const decoded = Buffer.from(config.privateKey, "base64");
224
+ const text = decoded.toString("utf8");
225
+ if (text.includes("PRIVATE KEY")) {
226
+ privateKeyBuffer = decoded;
227
+ } else {
228
+ throw new Error(`SSH key is neither a valid file path nor a base64-encoded private key`);
229
+ }
230
+ } catch (decodeError) {
231
+ if (decodeError instanceof Error && decodeError.message.includes("neither a valid file path")) {
232
+ throw decodeError;
233
+ }
234
+ throw new Error(`SSH key is neither a valid file path nor a base64-encoded private key`);
235
+ }
268
236
  }
269
237
  }
270
238
  if (!config.password && !privateKeyBuffer) {
@@ -514,208 +482,6 @@ import dotenv from "dotenv";
514
482
  import path from "path";
515
483
  import fs from "fs";
516
484
  import { fileURLToPath } from "url";
517
-
518
- // src/utils/safe-url.ts
519
- var SafeURL = class {
520
- /**
521
- * Parse a URL and handle special characters in passwords
522
- * This is a safe alternative to the URL constructor
523
- *
524
- * @param urlString - The DSN string to parse
525
- */
526
- constructor(urlString) {
527
- this.protocol = "";
528
- this.hostname = "";
529
- this.port = "";
530
- this.pathname = "";
531
- this.username = "";
532
- this.password = "";
533
- this.searchParams = /* @__PURE__ */ new Map();
534
- if (!urlString || urlString.trim() === "") {
535
- throw new Error("URL string cannot be empty");
536
- }
537
- try {
538
- const protocolSeparator = urlString.indexOf("://");
539
- if (protocolSeparator !== -1) {
540
- this.protocol = urlString.substring(0, protocolSeparator + 1);
541
- urlString = urlString.substring(protocolSeparator + 3);
542
- } else {
543
- throw new Error('Invalid URL format: missing protocol (e.g., "mysql://")');
544
- }
545
- const questionMarkIndex = urlString.indexOf("?");
546
- let queryParams = "";
547
- if (questionMarkIndex !== -1) {
548
- queryParams = urlString.substring(questionMarkIndex + 1);
549
- urlString = urlString.substring(0, questionMarkIndex);
550
- queryParams.split("&").forEach((pair) => {
551
- const parts = pair.split("=");
552
- if (parts.length === 2 && parts[0] && parts[1]) {
553
- this.searchParams.set(parts[0], decodeURIComponent(parts[1]));
554
- }
555
- });
556
- }
557
- const atIndex = urlString.indexOf("@");
558
- if (atIndex !== -1) {
559
- const auth = urlString.substring(0, atIndex);
560
- urlString = urlString.substring(atIndex + 1);
561
- const colonIndex2 = auth.indexOf(":");
562
- if (colonIndex2 !== -1) {
563
- this.username = auth.substring(0, colonIndex2);
564
- this.password = auth.substring(colonIndex2 + 1);
565
- this.username = decodeURIComponent(this.username);
566
- this.password = decodeURIComponent(this.password);
567
- } else {
568
- this.username = auth;
569
- }
570
- }
571
- const pathSeparatorIndex = urlString.indexOf("/");
572
- if (pathSeparatorIndex !== -1) {
573
- this.pathname = urlString.substring(pathSeparatorIndex);
574
- urlString = urlString.substring(0, pathSeparatorIndex);
575
- }
576
- const colonIndex = urlString.indexOf(":");
577
- if (colonIndex !== -1) {
578
- this.hostname = urlString.substring(0, colonIndex);
579
- this.port = urlString.substring(colonIndex + 1);
580
- } else {
581
- this.hostname = urlString;
582
- }
583
- if (this.protocol === "") {
584
- throw new Error("Invalid URL: protocol is required");
585
- }
586
- } catch (error) {
587
- throw new Error(`Failed to parse URL: ${error instanceof Error ? error.message : String(error)}`);
588
- }
589
- }
590
- /**
591
- * Helper method to safely get a parameter from query string
592
- *
593
- * @param name - The parameter name to retrieve
594
- * @returns The parameter value or null if not found
595
- */
596
- getSearchParam(name) {
597
- return this.searchParams.has(name) ? this.searchParams.get(name) : null;
598
- }
599
- /**
600
- * Helper method to iterate over all parameters
601
- *
602
- * @param callback - Function to call for each parameter
603
- */
604
- forEachSearchParam(callback) {
605
- this.searchParams.forEach((value, key) => callback(value, key));
606
- }
607
- };
608
-
609
- // src/utils/dsn-obfuscate.ts
610
- function parseConnectionInfoFromDSN(dsn) {
611
- if (!dsn) {
612
- return null;
613
- }
614
- try {
615
- const type = getDatabaseTypeFromDSN(dsn);
616
- if (typeof type === "undefined") {
617
- return null;
618
- }
619
- if (type === "sqlite") {
620
- const prefix = "sqlite:///";
621
- if (dsn.length > prefix.length) {
622
- const rawPath = dsn.substring(prefix.length);
623
- const firstChar = rawPath[0];
624
- const isWindowsDrive = rawPath.length > 1 && rawPath[1] === ":";
625
- const isSpecialPath = firstChar === ":" || firstChar === "." || firstChar === "~" || isWindowsDrive;
626
- return {
627
- type,
628
- database: isSpecialPath ? rawPath : "/" + rawPath
629
- };
630
- }
631
- return { type };
632
- }
633
- const url = new SafeURL(dsn);
634
- const info = { type };
635
- if (url.hostname) {
636
- info.host = url.hostname;
637
- }
638
- if (url.port) {
639
- info.port = parseInt(url.port, 10);
640
- }
641
- if (url.pathname && url.pathname.length > 1) {
642
- info.database = url.pathname.substring(1);
643
- }
644
- if (url.username) {
645
- info.user = url.username;
646
- }
647
- return info;
648
- } catch {
649
- return null;
650
- }
651
- }
652
- function obfuscateDSNPassword(dsn) {
653
- if (!dsn) {
654
- return dsn;
655
- }
656
- try {
657
- const type = getDatabaseTypeFromDSN(dsn);
658
- if (type === "sqlite") {
659
- return dsn;
660
- }
661
- const url = new SafeURL(dsn);
662
- if (!url.password) {
663
- return dsn;
664
- }
665
- const obfuscatedPassword = "*".repeat(Math.min(url.password.length, 8));
666
- const protocol = dsn.split(":")[0];
667
- let result;
668
- if (url.username) {
669
- result = `${protocol}://${url.username}:${obfuscatedPassword}@${url.hostname}`;
670
- } else {
671
- result = `${protocol}://${obfuscatedPassword}@${url.hostname}`;
672
- }
673
- if (url.port) {
674
- result += `:${url.port}`;
675
- }
676
- result += url.pathname;
677
- if (url.searchParams.size > 0) {
678
- const params = [];
679
- url.forEachSearchParam((value, key) => {
680
- params.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
681
- });
682
- result += `?${params.join("&")}`;
683
- }
684
- return result;
685
- } catch {
686
- return dsn;
687
- }
688
- }
689
- function getDatabaseTypeFromDSN(dsn) {
690
- if (!dsn) {
691
- return void 0;
692
- }
693
- const protocol = dsn.split(":")[0];
694
- return protocolToConnectorType(protocol);
695
- }
696
- function protocolToConnectorType(protocol) {
697
- const mapping = {
698
- "postgres": "postgres",
699
- "postgresql": "postgres",
700
- "mysql": "mysql",
701
- "mariadb": "mariadb",
702
- "sqlserver": "sqlserver",
703
- "sqlite": "sqlite"
704
- };
705
- return mapping[protocol];
706
- }
707
- function getDefaultPortForType(type) {
708
- const ports = {
709
- "postgres": 5432,
710
- "mysql": 3306,
711
- "mariadb": 3306,
712
- "sqlserver": 1433,
713
- "sqlite": void 0
714
- };
715
- return ports[type];
716
- }
717
-
718
- // src/config/env.ts
719
485
  var __filename = fileURLToPath(import.meta.url);
720
486
  var __dirname = path.dirname(__filename);
721
487
  function parseCommandLineArgs() {
@@ -1974,216 +1740,6 @@ var ConnectorManager = class {
1974
1740
  }
1975
1741
  };
1976
1742
 
1977
- // src/utils/sql-parser.ts
1978
- var TokenType = { Plain: 0, Comment: 1, QuotedBlock: 2 };
1979
- function plainToken(i) {
1980
- return { type: TokenType.Plain, end: i + 1 };
1981
- }
1982
- function scanSingleLineComment(sql, i) {
1983
- if (sql[i] !== "-" || sql[i + 1] !== "-") {
1984
- return null;
1985
- }
1986
- let j = i;
1987
- while (j < sql.length && sql[j] !== "\n") {
1988
- j++;
1989
- }
1990
- return { type: TokenType.Comment, end: j };
1991
- }
1992
- function scanMultiLineComment(sql, i) {
1993
- if (sql[i] !== "/" || sql[i + 1] !== "*") {
1994
- return null;
1995
- }
1996
- let j = i + 2;
1997
- while (j < sql.length && !(sql[j] === "*" && sql[j + 1] === "/")) {
1998
- j++;
1999
- }
2000
- if (j < sql.length) {
2001
- j += 2;
2002
- }
2003
- return { type: TokenType.Comment, end: j };
2004
- }
2005
- function scanNestedMultiLineComment(sql, i) {
2006
- if (sql[i] !== "/" || sql[i + 1] !== "*") {
2007
- return null;
2008
- }
2009
- let j = i + 2;
2010
- let depth = 1;
2011
- while (j < sql.length && depth > 0) {
2012
- if (sql[j] === "/" && sql[j + 1] === "*") {
2013
- depth++;
2014
- j += 2;
2015
- } else if (sql[j] === "*" && sql[j + 1] === "/") {
2016
- depth--;
2017
- j += 2;
2018
- } else {
2019
- j++;
2020
- }
2021
- }
2022
- return { type: TokenType.Comment, end: j };
2023
- }
2024
- function scanSingleQuotedString(sql, i) {
2025
- if (sql[i] !== "'") {
2026
- return null;
2027
- }
2028
- let j = i + 1;
2029
- while (j < sql.length) {
2030
- if (sql[j] === "'" && sql[j + 1] === "'") {
2031
- j += 2;
2032
- } else if (sql[j] === "'") {
2033
- j++;
2034
- break;
2035
- } else {
2036
- j++;
2037
- }
2038
- }
2039
- return { type: TokenType.QuotedBlock, end: j };
2040
- }
2041
- function scanDoubleQuotedString(sql, i) {
2042
- if (sql[i] !== '"') {
2043
- return null;
2044
- }
2045
- let j = i + 1;
2046
- while (j < sql.length) {
2047
- if (sql[j] === '"' && sql[j + 1] === '"') {
2048
- j += 2;
2049
- } else if (sql[j] === '"') {
2050
- j++;
2051
- break;
2052
- } else {
2053
- j++;
2054
- }
2055
- }
2056
- return { type: TokenType.QuotedBlock, end: j };
2057
- }
2058
- var dollarQuoteOpenRegex = /^\$([a-zA-Z_]\w*)?\$/;
2059
- function scanDollarQuotedBlock(sql, i) {
2060
- if (sql[i] !== "$") {
2061
- return null;
2062
- }
2063
- const next = sql[i + 1];
2064
- if (next >= "0" && next <= "9") {
2065
- return null;
2066
- }
2067
- const remaining = sql.substring(i);
2068
- const m = dollarQuoteOpenRegex.exec(remaining);
2069
- if (!m) {
2070
- return null;
2071
- }
2072
- const tag = m[0];
2073
- const bodyStart = i + tag.length;
2074
- const closeIdx = sql.indexOf(tag, bodyStart);
2075
- const end = closeIdx !== -1 ? closeIdx + tag.length : sql.length;
2076
- return { type: TokenType.QuotedBlock, end };
2077
- }
2078
- function scanBacktickQuotedIdentifier(sql, i) {
2079
- if (sql[i] !== "`") {
2080
- return null;
2081
- }
2082
- let j = i + 1;
2083
- while (j < sql.length) {
2084
- if (sql[j] === "`" && sql[j + 1] === "`") {
2085
- j += 2;
2086
- } else if (sql[j] === "`") {
2087
- j++;
2088
- break;
2089
- } else {
2090
- j++;
2091
- }
2092
- }
2093
- return { type: TokenType.QuotedBlock, end: j };
2094
- }
2095
- function scanBracketQuotedIdentifier(sql, i) {
2096
- if (sql[i] !== "[") {
2097
- return null;
2098
- }
2099
- let j = i + 1;
2100
- while (j < sql.length) {
2101
- if (sql[j] === "]" && sql[j + 1] === "]") {
2102
- j += 2;
2103
- } else if (sql[j] === "]") {
2104
- j++;
2105
- break;
2106
- } else {
2107
- j++;
2108
- }
2109
- }
2110
- return { type: TokenType.QuotedBlock, end: j };
2111
- }
2112
- function scanTokenAnsi(sql, i) {
2113
- return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? plainToken(i);
2114
- }
2115
- function scanTokenPostgres(sql, i) {
2116
- return scanSingleLineComment(sql, i) ?? scanNestedMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanDollarQuotedBlock(sql, i) ?? plainToken(i);
2117
- }
2118
- function scanTokenMySQL(sql, i) {
2119
- return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? plainToken(i);
2120
- }
2121
- function scanTokenSQLite(sql, i) {
2122
- return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? scanBracketQuotedIdentifier(sql, i) ?? plainToken(i);
2123
- }
2124
- function scanTokenSQLServer(sql, i) {
2125
- return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBracketQuotedIdentifier(sql, i) ?? plainToken(i);
2126
- }
2127
- var dialectScanners = {
2128
- postgres: scanTokenPostgres,
2129
- mysql: scanTokenMySQL,
2130
- mariadb: scanTokenMySQL,
2131
- sqlite: scanTokenSQLite,
2132
- sqlserver: scanTokenSQLServer
2133
- };
2134
- function getScanner(dialect) {
2135
- return dialect ? dialectScanners[dialect] ?? scanTokenAnsi : scanTokenAnsi;
2136
- }
2137
- function stripCommentsAndStrings(sql, dialect) {
2138
- const scanToken = getScanner(dialect);
2139
- const parts = [];
2140
- let plainStart = -1;
2141
- let i = 0;
2142
- while (i < sql.length) {
2143
- const token = scanToken(sql, i);
2144
- if (token.type === TokenType.Plain) {
2145
- if (plainStart === -1) {
2146
- plainStart = i;
2147
- }
2148
- } else {
2149
- if (plainStart !== -1) {
2150
- parts.push(sql.substring(plainStart, i));
2151
- plainStart = -1;
2152
- }
2153
- parts.push(" ");
2154
- }
2155
- i = token.end;
2156
- }
2157
- if (plainStart !== -1) {
2158
- parts.push(sql.substring(plainStart));
2159
- }
2160
- return parts.join("");
2161
- }
2162
- function splitSQLStatements(sql, dialect) {
2163
- const scanToken = getScanner(dialect);
2164
- const statements = [];
2165
- let stmtStart = 0;
2166
- let i = 0;
2167
- while (i < sql.length) {
2168
- if (sql[i] === ";") {
2169
- const trimmed2 = sql.substring(stmtStart, i).trim();
2170
- if (trimmed2.length > 0) {
2171
- statements.push(trimmed2);
2172
- }
2173
- stmtStart = i + 1;
2174
- i++;
2175
- continue;
2176
- }
2177
- const token = scanToken(sql, i);
2178
- i = token.end;
2179
- }
2180
- const trimmed = sql.substring(stmtStart).trim();
2181
- if (trimmed.length > 0) {
2182
- statements.push(trimmed);
2183
- }
2184
- return statements;
2185
- }
2186
-
2187
1743
  // src/utils/parameter-mapper.ts
2188
1744
  var PARAMETER_STYLES = {
2189
1745
  postgres: "numbered",
@@ -2510,14 +2066,6 @@ function getToolRegistry() {
2510
2066
  }
2511
2067
 
2512
2068
  export {
2513
- ConnectorRegistry,
2514
- SafeURL,
2515
- parseConnectionInfoFromDSN,
2516
- obfuscateDSNPassword,
2517
- getDatabaseTypeFromDSN,
2518
- getDefaultPortForType,
2519
- stripCommentsAndStrings,
2520
- splitSQLStatements,
2521
2069
  isDemoMode,
2522
2070
  resolveTransport,
2523
2071
  resolvePort,
@@ -0,0 +1,34 @@
1
+ // src/utils/identifier-quoter.ts
2
+ function quoteIdentifier(identifier, dbType) {
3
+ if (/[\0\x08\x09\x1a\n\r]/.test(identifier)) {
4
+ throw new Error(`Invalid identifier: contains control characters: ${identifier}`);
5
+ }
6
+ if (!identifier) {
7
+ throw new Error("Identifier cannot be empty");
8
+ }
9
+ switch (dbType) {
10
+ case "postgres":
11
+ case "sqlite":
12
+ return `"${identifier.replace(/"/g, '""')}"`;
13
+ case "mysql":
14
+ case "mariadb":
15
+ return `\`${identifier.replace(/`/g, "``")}\``;
16
+ case "sqlserver":
17
+ return `[${identifier.replace(/]/g, "]]")}]`;
18
+ default:
19
+ return `"${identifier.replace(/"/g, '""')}"`;
20
+ }
21
+ }
22
+ function quoteQualifiedIdentifier(tableName, schemaName, dbType) {
23
+ const quotedTable = quoteIdentifier(tableName, dbType);
24
+ if (schemaName) {
25
+ const quotedSchema = quoteIdentifier(schemaName, dbType);
26
+ return `${quotedSchema}.${quotedTable}`;
27
+ }
28
+ return quotedTable;
29
+ }
30
+
31
+ export {
32
+ quoteIdentifier,
33
+ quoteQualifiedIdentifier
34
+ };
@@ -0,0 +1,60 @@
1
+ // src/utils/multi-statement-result-parser.ts
2
+ function isMetadataObject(element) {
3
+ if (!element || typeof element !== "object" || Array.isArray(element)) {
4
+ return false;
5
+ }
6
+ return "affectedRows" in element || "insertId" in element || "fieldCount" in element || "warningStatus" in element;
7
+ }
8
+ function isMultiStatementResult(results) {
9
+ if (!Array.isArray(results) || results.length === 0) {
10
+ return false;
11
+ }
12
+ const firstElement = results[0];
13
+ return isMetadataObject(firstElement) || Array.isArray(firstElement);
14
+ }
15
+ function extractRowsFromMultiStatement(results) {
16
+ if (!Array.isArray(results)) {
17
+ return [];
18
+ }
19
+ const allRows = [];
20
+ for (const result of results) {
21
+ if (Array.isArray(result)) {
22
+ allRows.push(...result);
23
+ }
24
+ }
25
+ return allRows;
26
+ }
27
+ function extractAffectedRows(results) {
28
+ if (isMetadataObject(results)) {
29
+ return results.affectedRows || 0;
30
+ }
31
+ if (!Array.isArray(results)) {
32
+ return 0;
33
+ }
34
+ if (isMultiStatementResult(results)) {
35
+ let totalAffected = 0;
36
+ for (const result of results) {
37
+ if (isMetadataObject(result)) {
38
+ totalAffected += result.affectedRows || 0;
39
+ } else if (Array.isArray(result)) {
40
+ totalAffected += result.length;
41
+ }
42
+ }
43
+ return totalAffected;
44
+ }
45
+ return results.length;
46
+ }
47
+ function parseQueryResults(results) {
48
+ if (!Array.isArray(results)) {
49
+ return [];
50
+ }
51
+ if (isMultiStatementResult(results)) {
52
+ return extractRowsFromMultiStatement(results);
53
+ }
54
+ return results;
55
+ }
56
+
57
+ export {
58
+ extractAffectedRows,
59
+ parseQueryResults
60
+ };