@bytebase/dbhub 0.21.1 → 0.22.1
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.
- package/README.md +14 -4
- package/dist/{chunk-6JFMPX62.js → chunk-MEBWJMLY.js} +106 -3
- package/dist/{chunk-GWPUVYLO.js → chunk-WVVMH6FJ.js} +14 -0
- package/dist/index.js +162 -17
- package/dist/{mariadb-VGTS4WXE.js → mariadb-DVQM4MMM.js} +37 -4
- package/dist/{mysql-I35IQ2GH.js → mysql-NCKXNVZJ.js} +37 -4
- package/dist/{postgres-4PCNQDGV.js → postgres-EKVOBAH6.js} +83 -21
- package/dist/{registry-I2JQWFDX.js → registry-FZPB5LRL.js} +2 -2
- package/dist/{sqlite-FSCLCRIH.js → sqlite-2GBIF3MQ.js} +111 -33
- package/dist/{sqlserver-JBR6X37Z.js → sqlserver-5E7DSDVX.js} +37 -2
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ docker run --rm --init \
|
|
|
74
74
|
--dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
**NPM:**
|
|
77
|
+
**NPM:** (requires Node.js >= 22.5.0)
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
80
|
npx @bytebase/dbhub@latest --transport http --port 8080 --dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
@@ -86,6 +86,14 @@ npx @bytebase/dbhub@latest --transport http --port 8080 --dsn "postgres://user:p
|
|
|
86
86
|
npx @bytebase/dbhub@latest --transport http --port 8080 --demo
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
**Restrict to loopback (recommended for production):**
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx @bytebase/dbhub@latest --transport http --host 127.0.0.1 --port 8080 --demo
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> The HTTP transport defaults to `--host 0.0.0.0`, exposing DBHub on every network interface. For production, bind to `127.0.0.1` and front DBHub with a reverse proxy (nginx/Caddy) or firewall — DBHub does not authenticate HTTP clients.
|
|
96
|
+
|
|
89
97
|
See [Command-Line Options](https://dbhub.ai/config/command-line) for all available parameters.
|
|
90
98
|
|
|
91
99
|
### Multi-Database Setup
|
|
@@ -96,6 +104,8 @@ See [Multi-Database Configuration](https://dbhub.ai/config/toml) for complete se
|
|
|
96
104
|
|
|
97
105
|
## Development
|
|
98
106
|
|
|
107
|
+
Requires Node.js >= 22.5.0 (DBHub uses the built-in `node:sqlite` module).
|
|
108
|
+
|
|
99
109
|
```bash
|
|
100
110
|
# Install dependencies
|
|
101
111
|
pnpm install
|
|
@@ -119,8 +129,8 @@ See [Testing](.claude/skills/testing/SKILL.md) and [Debug](https://dbhub.ai/conf
|
|
|
119
129
|
|
|
120
130
|
<a href="https://www.star-history.com/?repos=bytebase%2Fdbhub&type=date&legend=top-left">
|
|
121
131
|
<picture>
|
|
122
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/
|
|
123
|
-
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/
|
|
124
|
-
<img alt="Star History Chart" src="https://api.star-history.com/
|
|
132
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=bytebase/dbhub&type=date&theme=dark&legend=top-left" />
|
|
133
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=bytebase/dbhub&type=date&legend=top-left" />
|
|
134
|
+
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=bytebase/dbhub&type=date&legend=top-left" />
|
|
125
135
|
</picture>
|
|
126
136
|
</a>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isDriverNotInstalled
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-WVVMH6FJ.js";
|
|
4
4
|
import {
|
|
5
5
|
ConnectorRegistry,
|
|
6
6
|
SafeURL,
|
|
@@ -701,6 +701,38 @@ function resolvePort() {
|
|
|
701
701
|
}
|
|
702
702
|
return { port: 8080, source: "default" };
|
|
703
703
|
}
|
|
704
|
+
function requireFlagValue(flag, args, example) {
|
|
705
|
+
const fail = () => {
|
|
706
|
+
console.error(`ERROR: --${flag} requires a value (e.g., --${flag}=${example}).`);
|
|
707
|
+
process.exit(1);
|
|
708
|
+
};
|
|
709
|
+
const rawArgs = process.argv.slice(2);
|
|
710
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
711
|
+
const token = rawArgs[i];
|
|
712
|
+
if (token === `--${flag}`) {
|
|
713
|
+
const next = rawArgs[i + 1];
|
|
714
|
+
if (!next || next.startsWith("--")) fail();
|
|
715
|
+
} else if (token === `--${flag}=`) {
|
|
716
|
+
fail();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (args[flag] === void 0) return void 0;
|
|
720
|
+
const value = args[flag].trim();
|
|
721
|
+
if (!value) fail();
|
|
722
|
+
return value;
|
|
723
|
+
}
|
|
724
|
+
function resolveHost() {
|
|
725
|
+
const args = parseCommandLineArgs();
|
|
726
|
+
const cliHost = requireFlagValue("host", args, "127.0.0.1");
|
|
727
|
+
if (cliHost !== void 0) {
|
|
728
|
+
return { host: cliHost, source: "command line argument" };
|
|
729
|
+
}
|
|
730
|
+
const envHost = process.env.DBHUB_HOST?.trim();
|
|
731
|
+
if (envHost) {
|
|
732
|
+
return { host: envHost, source: "environment variable" };
|
|
733
|
+
}
|
|
734
|
+
return { host: "0.0.0.0", source: "default" };
|
|
735
|
+
}
|
|
704
736
|
function redactDSN(dsn) {
|
|
705
737
|
try {
|
|
706
738
|
const url = new URL(dsn);
|
|
@@ -1132,12 +1164,45 @@ function validateSourceConfig(source, configPath) {
|
|
|
1132
1164
|
`Configuration file ${configPath}: source '${source.id}' has sslmode but SQLite does not support SSL. Remove the sslmode field for SQLite sources.`
|
|
1133
1165
|
);
|
|
1134
1166
|
}
|
|
1135
|
-
const validSslModes = ["disable", "require"];
|
|
1167
|
+
const validSslModes = ["disable", "require", "verify-ca", "verify-full"];
|
|
1136
1168
|
if (!validSslModes.includes(source.sslmode)) {
|
|
1137
1169
|
throw new Error(
|
|
1138
1170
|
`Configuration file ${configPath}: source '${source.id}' has invalid sslmode '${source.sslmode}'. Valid values: ${validSslModes.join(", ")}`
|
|
1139
1171
|
);
|
|
1140
1172
|
}
|
|
1173
|
+
if ((source.sslmode === "verify-ca" || source.sslmode === "verify-full") && source.type !== "postgres") {
|
|
1174
|
+
throw new Error(
|
|
1175
|
+
`Configuration file ${configPath}: source '${source.id}' has sslmode '${source.sslmode}' which is only supported for PostgreSQL. Valid values for ${source.type}: disable, require`
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (source.sslrootcert !== void 0) {
|
|
1180
|
+
if (source.sslmode !== "verify-ca" && source.sslmode !== "verify-full") {
|
|
1181
|
+
throw new Error(
|
|
1182
|
+
`Configuration file ${configPath}: source '${source.id}' has sslrootcert but sslmode is '${source.sslmode ?? "not set"}'. sslrootcert requires sslmode 'verify-ca' or 'verify-full'`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
const expandedPath = expandHomeDir(source.sslrootcert);
|
|
1186
|
+
let stats;
|
|
1187
|
+
try {
|
|
1188
|
+
stats = fs2.statSync(expandedPath);
|
|
1189
|
+
} catch {
|
|
1190
|
+
throw new Error(
|
|
1191
|
+
`Configuration file ${configPath}: source '${source.id}' sslrootcert file not found or not accessible: '${expandedPath}'`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
if (!stats.isFile()) {
|
|
1195
|
+
throw new Error(
|
|
1196
|
+
`Configuration file ${configPath}: source '${source.id}' sslrootcert path is not a regular file: '${expandedPath}'`
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
try {
|
|
1200
|
+
fs2.accessSync(expandedPath, fs2.constants.R_OK);
|
|
1201
|
+
} catch {
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
`Configuration file ${configPath}: source '${source.id}' sslrootcert file is not readable: '${expandedPath}'`
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1141
1206
|
}
|
|
1142
1207
|
if (source.authentication !== void 0) {
|
|
1143
1208
|
if (source.type !== "sqlserver") {
|
|
@@ -1186,6 +1251,18 @@ function validateSourceConfig(source, configPath) {
|
|
|
1186
1251
|
);
|
|
1187
1252
|
}
|
|
1188
1253
|
}
|
|
1254
|
+
if (source.timezone !== void 0) {
|
|
1255
|
+
if (source.type !== "mysql" && source.type !== "mariadb") {
|
|
1256
|
+
throw new Error(
|
|
1257
|
+
`Configuration file ${configPath}: source '${source.id}' has 'timezone' but it is only supported for MySQL and MariaDB sources.`
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
if (typeof source.timezone !== "string" || !/^(?:local|Z|[+-]\d\d:\d\d)$/.test(source.timezone)) {
|
|
1261
|
+
throw new Error(
|
|
1262
|
+
`Configuration file ${configPath}: source '${source.id}' has invalid timezone '${source.timezone}'. Must be "local", "Z" (UTC), or an offset like "+09:00".`
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1189
1266
|
if (source.readonly !== void 0) {
|
|
1190
1267
|
throw new Error(
|
|
1191
1268
|
`Configuration file ${configPath}: source '${source.id}' has 'readonly' field, but readonly must be configured per-tool, not per-source. Move 'readonly' to [[tools]] configuration instead.`
|
|
@@ -1203,6 +1280,9 @@ function processSourceConfigs(sources, configPath) {
|
|
|
1203
1280
|
if (processed.ssh_key) {
|
|
1204
1281
|
processed.ssh_key = expandHomeDir(processed.ssh_key);
|
|
1205
1282
|
}
|
|
1283
|
+
if (processed.sslrootcert) {
|
|
1284
|
+
processed.sslrootcert = expandHomeDir(processed.sslrootcert);
|
|
1285
|
+
}
|
|
1206
1286
|
if (processed.type === "sqlite" && processed.database) {
|
|
1207
1287
|
processed.database = expandHomeDir(processed.database);
|
|
1208
1288
|
}
|
|
@@ -1228,6 +1308,18 @@ function processSourceConfigs(sources, configPath) {
|
|
|
1228
1308
|
processed.user = connectionInfo.user;
|
|
1229
1309
|
}
|
|
1230
1310
|
}
|
|
1311
|
+
try {
|
|
1312
|
+
const url = new SafeURL(processed.dsn);
|
|
1313
|
+
const dsnSslmode = url.getSearchParam("sslmode");
|
|
1314
|
+
if (!processed.sslmode && dsnSslmode) {
|
|
1315
|
+
processed.sslmode = dsnSslmode;
|
|
1316
|
+
}
|
|
1317
|
+
const dsnSslrootcert = url.getSearchParam("sslrootcert");
|
|
1318
|
+
if (!processed.sslrootcert && dsnSslrootcert) {
|
|
1319
|
+
processed.sslrootcert = dsnSslrootcert;
|
|
1320
|
+
}
|
|
1321
|
+
} catch {
|
|
1322
|
+
}
|
|
1231
1323
|
}
|
|
1232
1324
|
return processed;
|
|
1233
1325
|
});
|
|
@@ -1310,6 +1402,10 @@ function buildDSNFromSource(source) {
|
|
|
1310
1402
|
if (source.sslmode && source.type !== "sqlite") {
|
|
1311
1403
|
queryParams.push(`sslmode=${source.sslmode}`);
|
|
1312
1404
|
}
|
|
1405
|
+
if (source.sslrootcert && source.type === "postgres" && (source.sslmode === "verify-ca" || source.sslmode === "verify-full")) {
|
|
1406
|
+
const expandedCertPath = expandHomeDir(source.sslrootcert);
|
|
1407
|
+
queryParams.push(`sslrootcert=${encodeURIComponent(expandedCertPath)}`);
|
|
1408
|
+
}
|
|
1313
1409
|
if (queryParams.length > 0) {
|
|
1314
1410
|
dsn += `?${queryParams.join("&")}`;
|
|
1315
1411
|
}
|
|
@@ -1511,6 +1607,9 @@ var ConnectorManager = class {
|
|
|
1511
1607
|
if (source.search_path) {
|
|
1512
1608
|
config.searchPath = source.search_path;
|
|
1513
1609
|
}
|
|
1610
|
+
if (source.timezone) {
|
|
1611
|
+
config.timezone = source.timezone;
|
|
1612
|
+
}
|
|
1514
1613
|
await connector.connect(actualDSN, source.init_script, config);
|
|
1515
1614
|
this.connectors.set(sourceId, connector);
|
|
1516
1615
|
if (!this.sourceIds.includes(sourceId)) {
|
|
@@ -1741,7 +1840,10 @@ var ConnectorManager = class {
|
|
|
1741
1840
|
region: source.aws_region
|
|
1742
1841
|
});
|
|
1743
1842
|
const queryParams = new Map(parsed.searchParams);
|
|
1744
|
-
queryParams.
|
|
1843
|
+
const currentSslMode = queryParams.get("sslmode");
|
|
1844
|
+
if (currentSslMode !== "verify-ca" && currentSslMode !== "verify-full") {
|
|
1845
|
+
queryParams.set("sslmode", "require");
|
|
1846
|
+
}
|
|
1745
1847
|
const protocol = parsed.protocol.endsWith(":") ? parsed.protocol.slice(0, -1) : parsed.protocol;
|
|
1746
1848
|
const encodedUser = encodeURIComponent(username);
|
|
1747
1849
|
const encodedToken = encodeURIComponent(token);
|
|
@@ -2082,6 +2184,7 @@ export {
|
|
|
2082
2184
|
isDemoMode,
|
|
2083
2185
|
resolveTransport,
|
|
2084
2186
|
resolvePort,
|
|
2187
|
+
resolveHost,
|
|
2085
2188
|
resolveSourceConfigs,
|
|
2086
2189
|
BUILTIN_TOOL_EXECUTE_SQL,
|
|
2087
2190
|
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
@@ -11,6 +11,13 @@ function isDriverNotInstalled(err, driver) {
|
|
|
11
11
|
const missingSpecifier = match[1];
|
|
12
12
|
return missingSpecifier === driver || missingSpecifier.startsWith(`${driver}/`);
|
|
13
13
|
}
|
|
14
|
+
function isModuleNotFound(err) {
|
|
15
|
+
if (!(err instanceof Error) || !("code" in err)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const code = err.code;
|
|
19
|
+
return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
|
|
20
|
+
}
|
|
14
21
|
async function loadConnectors(connectorModules) {
|
|
15
22
|
await Promise.all(
|
|
16
23
|
connectorModules.map(async ({ load, name, driver }) => {
|
|
@@ -21,6 +28,13 @@ async function loadConnectors(connectorModules) {
|
|
|
21
28
|
console.error(
|
|
22
29
|
`Skipping ${name} connector: driver package "${driver}" not installed.`
|
|
23
30
|
);
|
|
31
|
+
} else if (isModuleNotFound(err)) {
|
|
32
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
33
|
+
const match = msg.match(MISSING_MODULE_RE);
|
|
34
|
+
const missing = match ? match[1] : "unknown";
|
|
35
|
+
console.error(
|
|
36
|
+
`Skipping ${name} connector: required dependency "${missing}" not installed.`
|
|
37
|
+
);
|
|
24
38
|
} else {
|
|
25
39
|
throw err;
|
|
26
40
|
}
|
package/dist/index.js
CHANGED
|
@@ -8,14 +8,15 @@ import {
|
|
|
8
8
|
isDemoMode,
|
|
9
9
|
loadTomlConfig,
|
|
10
10
|
mapArgumentsToArray,
|
|
11
|
+
resolveHost,
|
|
11
12
|
resolvePort,
|
|
12
13
|
resolveSourceConfigs,
|
|
13
14
|
resolveTomlConfigPath,
|
|
14
15
|
resolveTransport
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-MEBWJMLY.js";
|
|
16
17
|
import {
|
|
17
18
|
loadConnectors
|
|
18
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-WVVMH6FJ.js";
|
|
19
20
|
import {
|
|
20
21
|
quoteQualifiedIdentifier
|
|
21
22
|
} from "./chunk-JFWX35TB.js";
|
|
@@ -33,6 +34,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
33
34
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
34
35
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
35
36
|
import express from "express";
|
|
37
|
+
import http from "http";
|
|
36
38
|
import path from "path";
|
|
37
39
|
import { readFileSync } from "fs";
|
|
38
40
|
import { fileURLToPath } from "url";
|
|
@@ -281,7 +283,8 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
281
283
|
const responseData = {
|
|
282
284
|
rows: result.rows,
|
|
283
285
|
count: result.rowCount,
|
|
284
|
-
source_id: effectiveSourceId
|
|
286
|
+
source_id: effectiveSourceId,
|
|
287
|
+
...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
|
|
285
288
|
};
|
|
286
289
|
return createToolSuccessResponse(responseData);
|
|
287
290
|
} catch (error) {
|
|
@@ -307,7 +310,7 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
307
310
|
// src/tools/search-objects.ts
|
|
308
311
|
import { z as z2 } from "zod";
|
|
309
312
|
var searchDatabaseObjectsSchema = {
|
|
310
|
-
object_type: z2.enum(["schema", "table", "column", "procedure", "function", "index"]).describe("Object type to search"),
|
|
313
|
+
object_type: z2.enum(["schema", "table", "view", "column", "procedure", "function", "index"]).describe("Object type to search"),
|
|
311
314
|
pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char). Default: %"),
|
|
312
315
|
schema: z2.string().optional().describe("Filter to schema"),
|
|
313
316
|
table: z2.string().optional().describe("Filter to table (requires schema; column/index only)"),
|
|
@@ -451,6 +454,77 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
|
|
|
451
454
|
}
|
|
452
455
|
return results;
|
|
453
456
|
}
|
|
457
|
+
async function searchViews(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
458
|
+
const regex = likePatternToRegex(pattern);
|
|
459
|
+
const results = [];
|
|
460
|
+
let schemasToSearch;
|
|
461
|
+
if (schemaFilter) {
|
|
462
|
+
schemasToSearch = [schemaFilter];
|
|
463
|
+
} else {
|
|
464
|
+
schemasToSearch = await connector.getSchemas();
|
|
465
|
+
}
|
|
466
|
+
for (const schemaName of schemasToSearch) {
|
|
467
|
+
if (results.length >= limit) break;
|
|
468
|
+
try {
|
|
469
|
+
const views = await connector.getViews(schemaName);
|
|
470
|
+
const matched = views.filter((view) => regex.test(view));
|
|
471
|
+
for (const viewName of matched) {
|
|
472
|
+
if (results.length >= limit) break;
|
|
473
|
+
if (detailLevel === "names") {
|
|
474
|
+
results.push({
|
|
475
|
+
name: viewName,
|
|
476
|
+
schema: schemaName
|
|
477
|
+
});
|
|
478
|
+
} else if (detailLevel === "summary") {
|
|
479
|
+
try {
|
|
480
|
+
const columns = await connector.getTableSchema(viewName, schemaName);
|
|
481
|
+
const comment = await getTableComment(connector, viewName, schemaName);
|
|
482
|
+
results.push({
|
|
483
|
+
name: viewName,
|
|
484
|
+
schema: schemaName,
|
|
485
|
+
column_count: columns.length,
|
|
486
|
+
...comment ? { comment } : {}
|
|
487
|
+
});
|
|
488
|
+
} catch (error) {
|
|
489
|
+
results.push({
|
|
490
|
+
name: viewName,
|
|
491
|
+
schema: schemaName,
|
|
492
|
+
column_count: null
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
try {
|
|
497
|
+
const columns = await connector.getTableSchema(viewName, schemaName);
|
|
498
|
+
const comment = await getTableComment(connector, viewName, schemaName);
|
|
499
|
+
results.push({
|
|
500
|
+
name: viewName,
|
|
501
|
+
schema: schemaName,
|
|
502
|
+
column_count: columns.length,
|
|
503
|
+
...comment ? { comment } : {},
|
|
504
|
+
columns: columns.map((col) => ({
|
|
505
|
+
name: col.column_name,
|
|
506
|
+
type: col.data_type,
|
|
507
|
+
nullable: col.is_nullable === "YES",
|
|
508
|
+
default: col.column_default,
|
|
509
|
+
...col.description ? { description: col.description } : {}
|
|
510
|
+
})),
|
|
511
|
+
indexes: []
|
|
512
|
+
});
|
|
513
|
+
} catch (error) {
|
|
514
|
+
results.push({
|
|
515
|
+
name: viewName,
|
|
516
|
+
schema: schemaName,
|
|
517
|
+
error: `Unable to fetch full details: ${error.message}`
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
} catch (error) {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return results;
|
|
527
|
+
}
|
|
454
528
|
async function searchColumns(connector, pattern, schemaFilter, tableFilter, detailLevel, limit) {
|
|
455
529
|
const regex = likePatternToRegex(pattern);
|
|
456
530
|
const results = [];
|
|
@@ -467,7 +541,11 @@ async function searchColumns(connector, pattern, schemaFilter, tableFilter, deta
|
|
|
467
541
|
if (tableFilter) {
|
|
468
542
|
tablesToSearch = [tableFilter];
|
|
469
543
|
} else {
|
|
470
|
-
|
|
544
|
+
const [tables, views] = await Promise.all([
|
|
545
|
+
connector.getTables(schemaName),
|
|
546
|
+
Promise.resolve(connector.getViews(schemaName)).catch(() => [])
|
|
547
|
+
]);
|
|
548
|
+
tablesToSearch = [...tables, ...views ?? []];
|
|
471
549
|
}
|
|
472
550
|
for (const tableName of tablesToSearch) {
|
|
473
551
|
if (results.length >= limit) break;
|
|
@@ -649,6 +727,9 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
649
727
|
case "table":
|
|
650
728
|
results = await searchTables(connector, pattern, schema, detail_level, limit);
|
|
651
729
|
break;
|
|
730
|
+
case "view":
|
|
731
|
+
results = await searchViews(connector, pattern, schema, detail_level, limit);
|
|
732
|
+
break;
|
|
652
733
|
case "column":
|
|
653
734
|
results = await searchColumns(connector, pattern, schema, table, detail_level, limit);
|
|
654
735
|
break;
|
|
@@ -708,6 +789,11 @@ function normalizeSourceId(id) {
|
|
|
708
789
|
}
|
|
709
790
|
|
|
710
791
|
// src/utils/tool-metadata.ts
|
|
792
|
+
function buildSourceDescriptionPrefix(description) {
|
|
793
|
+
const trimmed = description?.trim() ?? "";
|
|
794
|
+
if (!trimmed) return "";
|
|
795
|
+
return /[.!?:]$/.test(trimmed) ? `${trimmed} ` : `${trimmed}. `;
|
|
796
|
+
}
|
|
711
797
|
function zodToParameters(schema) {
|
|
712
798
|
const parameters = [];
|
|
713
799
|
for (const [key, zodType] of Object.entries(schema)) {
|
|
@@ -747,9 +833,10 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
747
833
|
};
|
|
748
834
|
const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
|
|
749
835
|
const title = isSingleSource ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
|
|
836
|
+
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
750
837
|
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
751
838
|
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
|
|
752
|
-
const description = isSingleSource ?
|
|
839
|
+
const description = isSingleSource ? `${userDescPrefix}Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `${userDescPrefix}Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
|
|
753
840
|
const isReadonly = executeOptions.readonly === true;
|
|
754
841
|
const annotations = {
|
|
755
842
|
title,
|
|
@@ -776,7 +863,8 @@ function getSearchObjectsMetadata(sourceId) {
|
|
|
776
863
|
const isSingleSource = sourceIds.length === 1;
|
|
777
864
|
const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
|
|
778
865
|
const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
779
|
-
const
|
|
866
|
+
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
867
|
+
const description = isSingleSource ? `${userDescPrefix}Search and list database objects (schemas, tables, columns, procedures, functions, indexes) on the ${dbType} database` : `${userDescPrefix}Search and list database objects (schemas, tables, columns, procedures, functions, indexes) on the '${sourceId}' ${dbType} database`;
|
|
780
868
|
return {
|
|
781
869
|
name: toolName,
|
|
782
870
|
description,
|
|
@@ -1369,6 +1457,43 @@ Detected change in ${configPath}, reloading configuration...`);
|
|
|
1369
1457
|
};
|
|
1370
1458
|
}
|
|
1371
1459
|
|
|
1460
|
+
// src/utils/cross-origin.ts
|
|
1461
|
+
var INVALID_HOST_CHARS = /[\s/\\@?#]/;
|
|
1462
|
+
function validateOrigin(originHeader, hostHeader) {
|
|
1463
|
+
if (originHeader === void 0) return { ok: true };
|
|
1464
|
+
const trimmedOrigin = originHeader.trim();
|
|
1465
|
+
if (!trimmedOrigin) {
|
|
1466
|
+
return { ok: false, status: 400, message: "Malformed Origin header" };
|
|
1467
|
+
}
|
|
1468
|
+
const trimmedHost = (hostHeader ?? "").trim();
|
|
1469
|
+
if (!trimmedHost || INVALID_HOST_CHARS.test(trimmedHost)) {
|
|
1470
|
+
return { ok: false, status: 400, message: "Malformed Host header" };
|
|
1471
|
+
}
|
|
1472
|
+
let originHostname;
|
|
1473
|
+
try {
|
|
1474
|
+
originHostname = new URL(trimmedOrigin).hostname.toLowerCase();
|
|
1475
|
+
} catch {
|
|
1476
|
+
return { ok: false, status: 400, message: "Malformed Origin header" };
|
|
1477
|
+
}
|
|
1478
|
+
let hostname;
|
|
1479
|
+
try {
|
|
1480
|
+
hostname = new URL(`http://${trimmedHost}`).hostname.toLowerCase();
|
|
1481
|
+
} catch {
|
|
1482
|
+
return { ok: false, status: 400, message: "Malformed Host header" };
|
|
1483
|
+
}
|
|
1484
|
+
if (!hostname) {
|
|
1485
|
+
return { ok: false, status: 400, message: "Malformed Host header" };
|
|
1486
|
+
}
|
|
1487
|
+
if (originHostname !== hostname) {
|
|
1488
|
+
return {
|
|
1489
|
+
ok: false,
|
|
1490
|
+
status: 403,
|
|
1491
|
+
message: "Origin does not match Host header"
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
return { ok: true };
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1372
1497
|
// src/server.ts
|
|
1373
1498
|
var __filename = fileURLToPath(import.meta.url);
|
|
1374
1499
|
var __dirname = path.dirname(__filename);
|
|
@@ -1421,7 +1546,7 @@ See documentation for more details on configuring database connections.
|
|
|
1421
1546
|
const sources = sourceConfigsData.sources;
|
|
1422
1547
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1423
1548
|
await connectorManager.connectWithSources(sources);
|
|
1424
|
-
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-
|
|
1549
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-FZPB5LRL.js");
|
|
1425
1550
|
initializeToolRegistry2({
|
|
1426
1551
|
sources: sourceConfigsData.sources,
|
|
1427
1552
|
tools: sourceConfigsData.tools
|
|
@@ -1441,6 +1566,7 @@ See documentation for more details on configuring database connections.
|
|
|
1441
1566
|
};
|
|
1442
1567
|
const transportData = resolveTransport();
|
|
1443
1568
|
const port = transportData.type === "http" ? resolvePort().port : null;
|
|
1569
|
+
const host = transportData.type === "http" ? resolveHost().host : null;
|
|
1444
1570
|
const activeModes = [];
|
|
1445
1571
|
const modeDescriptions = [];
|
|
1446
1572
|
const isDemo = isDemoMode();
|
|
@@ -1466,6 +1592,13 @@ See documentation for more details on configuring database connections.
|
|
|
1466
1592
|
app.use(express.json());
|
|
1467
1593
|
app.use((req, res, next) => {
|
|
1468
1594
|
const origin = req.headers.origin;
|
|
1595
|
+
const result = validateOrigin(origin, req.headers.host);
|
|
1596
|
+
if (!result.ok) {
|
|
1597
|
+
return res.status(result.status).json({
|
|
1598
|
+
error: result.status === 400 ? "Bad Request" : "Forbidden",
|
|
1599
|
+
message: result.message
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1469
1602
|
res.header("Access-Control-Allow-Origin", origin || "http://localhost");
|
|
1470
1603
|
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1471
1604
|
res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
|
|
@@ -1512,16 +1645,28 @@ See documentation for more details on configuring database connections.
|
|
|
1512
1645
|
res.sendFile(path.join(frontendPath, "index.html"));
|
|
1513
1646
|
});
|
|
1514
1647
|
}
|
|
1515
|
-
|
|
1648
|
+
const httpServer = http.createServer(app);
|
|
1649
|
+
httpServer.on("error", (err) => {
|
|
1650
|
+
const displayHost = host.includes(":") ? `[${host}]` : host;
|
|
1651
|
+
console.error(`Failed to bind HTTP server to ${displayHost}:${port}: ${err.message}`);
|
|
1652
|
+
process.exit(1);
|
|
1653
|
+
});
|
|
1654
|
+
httpServer.listen(port, host, () => {
|
|
1655
|
+
const address = httpServer.address();
|
|
1656
|
+
const boundHost = typeof address === "object" && address ? address.address : host;
|
|
1657
|
+
const boundPort = typeof address === "object" && address ? address.port : port;
|
|
1658
|
+
const displayHost = boundHost.includes(":") ? `[${boundHost}]` : boundHost;
|
|
1659
|
+
const userHost = boundHost === "0.0.0.0" || boundHost === "::" ? "localhost" : displayHost;
|
|
1660
|
+
console.error(`HTTP server listening on ${displayHost}:${boundPort}`);
|
|
1516
1661
|
if (process.env.NODE_ENV === "development") {
|
|
1517
1662
|
console.error("Development mode detected!");
|
|
1518
1663
|
console.error(" Workbench dev server (with HMR): http://localhost:5173");
|
|
1519
|
-
console.error(
|
|
1664
|
+
console.error(` Backend API: http://localhost:${boundPort}`);
|
|
1520
1665
|
console.error("");
|
|
1521
1666
|
} else {
|
|
1522
|
-
console.error(`Workbench at http
|
|
1667
|
+
console.error(`Workbench at http://${userHost}:${boundPort}/`);
|
|
1523
1668
|
}
|
|
1524
|
-
console.error(`MCP server endpoint at http
|
|
1669
|
+
console.error(`MCP server endpoint at http://${userHost}:${boundPort}/mcp`);
|
|
1525
1670
|
});
|
|
1526
1671
|
} else {
|
|
1527
1672
|
const server = createServer();
|
|
@@ -1549,11 +1694,11 @@ See documentation for more details on configuring database connections.
|
|
|
1549
1694
|
|
|
1550
1695
|
// src/index.ts
|
|
1551
1696
|
var connectorModules = [
|
|
1552
|
-
{ load: () => import("./postgres-
|
|
1553
|
-
{ load: () => import("./sqlserver-
|
|
1554
|
-
{ load: () => import("./sqlite-
|
|
1555
|
-
{ load: () => import("./mysql-
|
|
1556
|
-
{ load: () => import("./mariadb-
|
|
1697
|
+
{ load: () => import("./postgres-EKVOBAH6.js"), name: "PostgreSQL", driver: "pg" },
|
|
1698
|
+
{ load: () => import("./sqlserver-5E7DSDVX.js"), name: "SQL Server", driver: "mssql" },
|
|
1699
|
+
{ load: () => import("./sqlite-2GBIF3MQ.js"), name: "SQLite", driver: "node:sqlite" },
|
|
1700
|
+
{ load: () => import("./mysql-NCKXNVZJ.js"), name: "MySQL", driver: "mysql2" },
|
|
1701
|
+
{ load: () => import("./mariadb-DVQM4MMM.js"), name: "MariaDB", driver: "mariadb" }
|
|
1557
1702
|
];
|
|
1558
1703
|
loadConnectors(connectorModules).then(() => main()).catch((error) => {
|
|
1559
1704
|
console.error("Fatal error:", error);
|
|
@@ -2,6 +2,9 @@ import {
|
|
|
2
2
|
extractAffectedRows,
|
|
3
3
|
parseQueryResults
|
|
4
4
|
} from "./chunk-RTB262PR.js";
|
|
5
|
+
import {
|
|
6
|
+
quoteIdentifier
|
|
7
|
+
} from "./chunk-JFWX35TB.js";
|
|
5
8
|
import {
|
|
6
9
|
SQLRowLimiter
|
|
7
10
|
} from "./chunk-BRXZ5ZQB.js";
|
|
@@ -43,6 +46,10 @@ Expected: ${expectedFormat}`
|
|
|
43
46
|
},
|
|
44
47
|
...queryTimeoutSeconds !== void 0 && {
|
|
45
48
|
queryTimeout: queryTimeoutSeconds * 1e3
|
|
49
|
+
},
|
|
50
|
+
// Controls how the driver interprets DATETIME values ("Z", "local", or "±HH:MM").
|
|
51
|
+
...config?.timezone !== void 0 && {
|
|
52
|
+
timezone: config.timezone
|
|
46
53
|
}
|
|
47
54
|
};
|
|
48
55
|
url.forEachSearchParam((value, key) => {
|
|
@@ -135,9 +142,10 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
135
142
|
const queryParams = schema ? [schema] : [];
|
|
136
143
|
const rows = await this.pool.query(
|
|
137
144
|
`
|
|
138
|
-
SELECT TABLE_NAME
|
|
139
|
-
FROM INFORMATION_SCHEMA.TABLES
|
|
145
|
+
SELECT TABLE_NAME
|
|
146
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
140
147
|
${schemaClause}
|
|
148
|
+
AND TABLE_TYPE = 'BASE TABLE'
|
|
141
149
|
ORDER BY TABLE_NAME
|
|
142
150
|
`,
|
|
143
151
|
queryParams
|
|
@@ -148,6 +156,29 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
148
156
|
throw error;
|
|
149
157
|
}
|
|
150
158
|
}
|
|
159
|
+
async getViews(schema) {
|
|
160
|
+
if (!this.pool) {
|
|
161
|
+
throw new Error("Not connected to database");
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
165
|
+
const queryParams = schema ? [schema] : [];
|
|
166
|
+
const rows = await this.pool.query(
|
|
167
|
+
`
|
|
168
|
+
SELECT TABLE_NAME
|
|
169
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
170
|
+
${schemaClause}
|
|
171
|
+
AND TABLE_TYPE = 'VIEW'
|
|
172
|
+
ORDER BY TABLE_NAME
|
|
173
|
+
`,
|
|
174
|
+
queryParams
|
|
175
|
+
);
|
|
176
|
+
return rows.map((row) => row.TABLE_NAME);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error("Error getting views:", error);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
151
182
|
async tableExists(tableName, schema) {
|
|
152
183
|
if (!this.pool) {
|
|
153
184
|
throw new Error("Not connected to database");
|
|
@@ -352,10 +383,12 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
352
383
|
let definition = procedure.ROUTINE_DEFINITION;
|
|
353
384
|
try {
|
|
354
385
|
const schemaValue = schema || await this.getCurrentSchema();
|
|
386
|
+
const quotedSchema = quoteIdentifier(schemaValue, "mariadb");
|
|
387
|
+
const quotedProcName = quoteIdentifier(procedureName, "mariadb");
|
|
355
388
|
if (procedure.procedure_type === "procedure") {
|
|
356
389
|
try {
|
|
357
390
|
const defRows = await this.pool.query(`
|
|
358
|
-
SHOW CREATE PROCEDURE ${
|
|
391
|
+
SHOW CREATE PROCEDURE ${quotedSchema}.${quotedProcName}
|
|
359
392
|
`);
|
|
360
393
|
if (defRows && defRows.length > 0) {
|
|
361
394
|
definition = defRows[0]["Create Procedure"];
|
|
@@ -366,7 +399,7 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
366
399
|
} else {
|
|
367
400
|
try {
|
|
368
401
|
const defRows = await this.pool.query(`
|
|
369
|
-
SHOW CREATE FUNCTION ${
|
|
402
|
+
SHOW CREATE FUNCTION ${quotedSchema}.${quotedProcName}
|
|
370
403
|
`);
|
|
371
404
|
if (defRows && defRows.length > 0) {
|
|
372
405
|
definition = defRows[0]["Create Function"];
|
|
@@ -2,6 +2,9 @@ import {
|
|
|
2
2
|
extractAffectedRows,
|
|
3
3
|
parseQueryResults
|
|
4
4
|
} from "./chunk-RTB262PR.js";
|
|
5
|
+
import {
|
|
6
|
+
quoteIdentifier
|
|
7
|
+
} from "./chunk-JFWX35TB.js";
|
|
5
8
|
import {
|
|
6
9
|
SQLRowLimiter
|
|
7
10
|
} from "./chunk-BRXZ5ZQB.js";
|
|
@@ -17,6 +20,7 @@ import mysql from "mysql2/promise";
|
|
|
17
20
|
var MySQLDSNParser = class {
|
|
18
21
|
async parse(dsn, config) {
|
|
19
22
|
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
|
|
23
|
+
const timezone = config?.timezone;
|
|
20
24
|
if (!this.isValidDSN(dsn)) {
|
|
21
25
|
const obfuscatedDSN = obfuscateDSNPassword(dsn);
|
|
22
26
|
const expectedFormat = this.getSampleDSN();
|
|
@@ -54,6 +58,9 @@ Expected: ${expectedFormat}`
|
|
|
54
58
|
if (connectionTimeoutSeconds !== void 0) {
|
|
55
59
|
config2.connectTimeout = connectionTimeoutSeconds * 1e3;
|
|
56
60
|
}
|
|
61
|
+
if (timezone !== void 0) {
|
|
62
|
+
config2.timezone = timezone;
|
|
63
|
+
}
|
|
57
64
|
if (url.password && url.password.includes("X-Amz-Credential")) {
|
|
58
65
|
config2.authPlugins = {
|
|
59
66
|
mysql_clear_password: () => () => {
|
|
@@ -141,9 +148,10 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
141
148
|
const queryParams = schema ? [schema] : [];
|
|
142
149
|
const [rows] = await this.pool.query(
|
|
143
150
|
`
|
|
144
|
-
SELECT TABLE_NAME
|
|
145
|
-
FROM INFORMATION_SCHEMA.TABLES
|
|
151
|
+
SELECT TABLE_NAME
|
|
152
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
146
153
|
${schemaClause}
|
|
154
|
+
AND TABLE_TYPE = 'BASE TABLE'
|
|
147
155
|
ORDER BY TABLE_NAME
|
|
148
156
|
`,
|
|
149
157
|
queryParams
|
|
@@ -154,6 +162,29 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
154
162
|
throw error;
|
|
155
163
|
}
|
|
156
164
|
}
|
|
165
|
+
async getViews(schema) {
|
|
166
|
+
if (!this.pool) {
|
|
167
|
+
throw new Error("Not connected to database");
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
171
|
+
const queryParams = schema ? [schema] : [];
|
|
172
|
+
const [rows] = await this.pool.query(
|
|
173
|
+
`
|
|
174
|
+
SELECT TABLE_NAME
|
|
175
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
176
|
+
${schemaClause}
|
|
177
|
+
AND TABLE_TYPE = 'VIEW'
|
|
178
|
+
ORDER BY TABLE_NAME
|
|
179
|
+
`,
|
|
180
|
+
queryParams
|
|
181
|
+
);
|
|
182
|
+
return rows.map((row) => row.TABLE_NAME);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error("Error getting views:", error);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
157
188
|
async tableExists(tableName, schema) {
|
|
158
189
|
if (!this.pool) {
|
|
159
190
|
throw new Error("Not connected to database");
|
|
@@ -358,10 +389,12 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
358
389
|
let definition = procedure.ROUTINE_DEFINITION;
|
|
359
390
|
try {
|
|
360
391
|
const schemaValue = schema || await this.getCurrentSchema();
|
|
392
|
+
const quotedSchema = quoteIdentifier(schemaValue, "mysql");
|
|
393
|
+
const quotedProcName = quoteIdentifier(procedureName, "mysql");
|
|
361
394
|
if (procedure.procedure_type === "procedure") {
|
|
362
395
|
try {
|
|
363
396
|
const [defRows] = await this.pool.query(`
|
|
364
|
-
SHOW CREATE PROCEDURE ${
|
|
397
|
+
SHOW CREATE PROCEDURE ${quotedSchema}.${quotedProcName}
|
|
365
398
|
`);
|
|
366
399
|
if (defRows && defRows.length > 0) {
|
|
367
400
|
definition = defRows[0]["Create Procedure"];
|
|
@@ -372,7 +405,7 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
372
405
|
} else {
|
|
373
406
|
try {
|
|
374
407
|
const [defRows] = await this.pool.query(`
|
|
375
|
-
SHOW CREATE FUNCTION ${
|
|
408
|
+
SHOW CREATE FUNCTION ${quotedSchema}.${quotedProcName}
|
|
376
409
|
`);
|
|
377
410
|
if (defRows && defRows.length > 0) {
|
|
378
411
|
definition = defRows[0]["Create Function"];
|
|
@@ -12,7 +12,20 @@ import {
|
|
|
12
12
|
} from "./chunk-C7WEAPX4.js";
|
|
13
13
|
|
|
14
14
|
// src/connectors/postgres/index.ts
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import os from "os";
|
|
17
|
+
import path from "path";
|
|
15
18
|
import pg from "pg";
|
|
19
|
+
|
|
20
|
+
// src/connectors/postgres/failed-to-read-certificate.ts
|
|
21
|
+
var FailedToReadCertificate = class extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "FailedToReadCertificate";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// src/connectors/postgres/index.ts
|
|
16
29
|
var { Pool } = pg;
|
|
17
30
|
var PostgresDSNParser = class {
|
|
18
31
|
async parse(dsn, config) {
|
|
@@ -37,17 +50,38 @@ Expected: ${expectedFormat}`
|
|
|
37
50
|
user: url.username,
|
|
38
51
|
password: url.password
|
|
39
52
|
};
|
|
53
|
+
let sslmode;
|
|
54
|
+
let sslrootcert;
|
|
40
55
|
url.forEachSearchParam((value, key) => {
|
|
41
56
|
if (key === "sslmode") {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
poolConfig.ssl = { rejectUnauthorized: false };
|
|
46
|
-
} else {
|
|
47
|
-
poolConfig.ssl = true;
|
|
48
|
-
}
|
|
57
|
+
sslmode = value;
|
|
58
|
+
} else if (key === "sslrootcert") {
|
|
59
|
+
sslrootcert = value;
|
|
49
60
|
}
|
|
50
61
|
});
|
|
62
|
+
if (sslmode === "disable") {
|
|
63
|
+
poolConfig.ssl = false;
|
|
64
|
+
} else if (sslmode === "require") {
|
|
65
|
+
poolConfig.ssl = { rejectUnauthorized: false };
|
|
66
|
+
} else if (sslmode === "verify-ca" || sslmode === "verify-full") {
|
|
67
|
+
const sslConfig = { rejectUnauthorized: true };
|
|
68
|
+
if (sslmode === "verify-ca") {
|
|
69
|
+
sslConfig.checkServerIdentity = () => void 0;
|
|
70
|
+
}
|
|
71
|
+
if (sslrootcert) {
|
|
72
|
+
const certPath = sslrootcert.startsWith("~/") ? path.join(os.homedir(), sslrootcert.slice(2)) : sslrootcert;
|
|
73
|
+
try {
|
|
74
|
+
sslConfig.ca = await fs.promises.readFile(certPath, "utf-8");
|
|
75
|
+
} catch (err) {
|
|
76
|
+
throw new FailedToReadCertificate(
|
|
77
|
+
`Failed to read SSL root certificate at '${certPath}': ${err instanceof Error ? err.message : String(err)}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
poolConfig.ssl = sslConfig;
|
|
82
|
+
} else if (sslmode !== void 0) {
|
|
83
|
+
poolConfig.ssl = true;
|
|
84
|
+
}
|
|
51
85
|
if (connectionTimeoutSeconds !== void 0) {
|
|
52
86
|
poolConfig.connectionTimeoutMillis = connectionTimeoutSeconds * 1e3;
|
|
53
87
|
}
|
|
@@ -56,8 +90,13 @@ Expected: ${expectedFormat}`
|
|
|
56
90
|
}
|
|
57
91
|
return poolConfig;
|
|
58
92
|
} catch (error) {
|
|
93
|
+
if (error instanceof FailedToReadCertificate) {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
const originalError = error instanceof Error ? error : new Error(String(error));
|
|
59
97
|
throw new Error(
|
|
60
|
-
`Failed to parse PostgreSQL DSN: ${
|
|
98
|
+
`Failed to parse PostgreSQL DSN: ${originalError.message}`,
|
|
99
|
+
{ cause: originalError }
|
|
61
100
|
);
|
|
62
101
|
}
|
|
63
102
|
}
|
|
@@ -145,9 +184,32 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
145
184
|
const schemaToUse = schema || this.defaultSchema;
|
|
146
185
|
const result = await client.query(
|
|
147
186
|
`
|
|
148
|
-
SELECT table_name
|
|
149
|
-
FROM information_schema.tables
|
|
187
|
+
SELECT table_name
|
|
188
|
+
FROM information_schema.tables
|
|
189
|
+
WHERE table_schema = $1
|
|
190
|
+
AND table_type = 'BASE TABLE'
|
|
191
|
+
ORDER BY table_name
|
|
192
|
+
`,
|
|
193
|
+
[schemaToUse]
|
|
194
|
+
);
|
|
195
|
+
return result.rows.map((row) => row.table_name);
|
|
196
|
+
} finally {
|
|
197
|
+
client.release();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async getViews(schema) {
|
|
201
|
+
if (!this.pool) {
|
|
202
|
+
throw new Error("Not connected to database");
|
|
203
|
+
}
|
|
204
|
+
const client = await this.pool.connect();
|
|
205
|
+
try {
|
|
206
|
+
const schemaToUse = schema || this.defaultSchema;
|
|
207
|
+
const result = await client.query(
|
|
208
|
+
`
|
|
209
|
+
SELECT table_name
|
|
210
|
+
FROM information_schema.tables
|
|
150
211
|
WHERE table_schema = $1
|
|
212
|
+
AND table_type = 'VIEW'
|
|
151
213
|
ORDER BY table_name
|
|
152
214
|
`,
|
|
153
215
|
[schemaToUse]
|
|
@@ -167,8 +229,8 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
167
229
|
const result = await client.query(
|
|
168
230
|
`
|
|
169
231
|
SELECT EXISTS (
|
|
170
|
-
SELECT FROM information_schema.tables
|
|
171
|
-
WHERE table_schema = $1
|
|
232
|
+
SELECT FROM information_schema.tables
|
|
233
|
+
WHERE table_schema = $1
|
|
172
234
|
AND table_name = $2
|
|
173
235
|
)
|
|
174
236
|
`,
|
|
@@ -188,18 +250,18 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
188
250
|
const schemaToUse = schema || this.defaultSchema;
|
|
189
251
|
const result = await client.query(
|
|
190
252
|
`
|
|
191
|
-
SELECT
|
|
253
|
+
SELECT
|
|
192
254
|
i.relname as index_name,
|
|
193
255
|
array_agg(a.attname) as column_names,
|
|
194
256
|
ix.indisunique as is_unique,
|
|
195
257
|
ix.indisprimary as is_primary
|
|
196
|
-
FROM
|
|
258
|
+
FROM
|
|
197
259
|
pg_class t,
|
|
198
260
|
pg_class i,
|
|
199
261
|
pg_index ix,
|
|
200
262
|
pg_attribute a,
|
|
201
263
|
pg_namespace ns
|
|
202
|
-
WHERE
|
|
264
|
+
WHERE
|
|
203
265
|
t.oid = ix.indrelid
|
|
204
266
|
AND i.oid = ix.indexrelid
|
|
205
267
|
AND a.attrelid = t.oid
|
|
@@ -208,11 +270,11 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
208
270
|
AND t.relname = $1
|
|
209
271
|
AND ns.oid = t.relnamespace
|
|
210
272
|
AND ns.nspname = $2
|
|
211
|
-
GROUP BY
|
|
212
|
-
i.relname,
|
|
273
|
+
GROUP BY
|
|
274
|
+
i.relname,
|
|
213
275
|
ix.indisunique,
|
|
214
276
|
ix.indisprimary
|
|
215
|
-
ORDER BY
|
|
277
|
+
ORDER BY
|
|
216
278
|
i.relname
|
|
217
279
|
`,
|
|
218
280
|
[tableName, schemaToUse]
|
|
@@ -353,7 +415,7 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
353
415
|
const schemaToUse = schema || this.defaultSchema;
|
|
354
416
|
const result = await client.query(
|
|
355
417
|
`
|
|
356
|
-
SELECT
|
|
418
|
+
SELECT
|
|
357
419
|
routine_name as procedure_name,
|
|
358
420
|
routine_type,
|
|
359
421
|
CASE WHEN routine_type = 'PROCEDURE' THEN 'procedure' ELSE 'function' END as procedure_type,
|
|
@@ -362,8 +424,8 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
362
424
|
routine_definition as definition,
|
|
363
425
|
(
|
|
364
426
|
SELECT string_agg(
|
|
365
|
-
parameter_name || ' ' ||
|
|
366
|
-
parameter_mode || ' ' ||
|
|
427
|
+
parameter_name || ' ' ||
|
|
428
|
+
parameter_mode || ' ' ||
|
|
367
429
|
data_type,
|
|
368
430
|
', '
|
|
369
431
|
)
|
|
@@ -11,8 +11,24 @@ import {
|
|
|
11
11
|
splitSQLStatements
|
|
12
12
|
} from "./chunk-C7WEAPX4.js";
|
|
13
13
|
|
|
14
|
+
// src/connectors/sqlite/suppress-experimental-warning.ts
|
|
15
|
+
var installed = false;
|
|
16
|
+
function suppressSqliteExperimentalWarning() {
|
|
17
|
+
if (installed) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
installed = true;
|
|
21
|
+
const originalEmitWarning = process.emitWarning.bind(process);
|
|
22
|
+
process.emitWarning = (warning, ...args) => {
|
|
23
|
+
const message = typeof warning === "string" ? warning : warning?.message;
|
|
24
|
+
if (message && message.includes("SQLite is an experimental feature")) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
return originalEmitWarning(warning, ...args);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
// src/connectors/sqlite/index.ts
|
|
15
|
-
import Database from "better-sqlite3";
|
|
16
32
|
var SQLiteDSNParser = class {
|
|
17
33
|
async parse(dsn, config) {
|
|
18
34
|
if (!this.isValidDSN(dsn)) {
|
|
@@ -73,6 +89,42 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
73
89
|
clone() {
|
|
74
90
|
return new _SQLiteConnector();
|
|
75
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Prepare a user-facing query statement with BigInt reads enabled, so 64-bit
|
|
94
|
+
* integer values in the result rows keep full precision.
|
|
95
|
+
*
|
|
96
|
+
* better-sqlite3 exposed this connection-wide via `defaultSafeIntegers(true)`;
|
|
97
|
+
* `node:sqlite` only offers it per-statement via `setReadBigInts`. This is for
|
|
98
|
+
* `executeSQL` only — schema introspection reads small integer flags and uses
|
|
99
|
+
* `queryAll`/`queryOne` (plain numbers) so comparisons like `=== 1` work.
|
|
100
|
+
*/
|
|
101
|
+
prepare(sql) {
|
|
102
|
+
if (!this.db) {
|
|
103
|
+
throw new Error("Not connected to SQLite database");
|
|
104
|
+
}
|
|
105
|
+
const statement = this.db.prepare(sql);
|
|
106
|
+
statement.setReadBigInts(true);
|
|
107
|
+
return statement;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Run an introspection query and return all rows. Integers come back as plain
|
|
111
|
+
* numbers (no `setReadBigInts`), which is what the schema-reading callers need
|
|
112
|
+
* for boolean flag comparisons. node:sqlite types `.all()` as a generic record
|
|
113
|
+
* array, so the result is cast to the caller's expected row shape.
|
|
114
|
+
*/
|
|
115
|
+
queryAll(sql, ...params) {
|
|
116
|
+
if (!this.db) {
|
|
117
|
+
throw new Error("Not connected to SQLite database");
|
|
118
|
+
}
|
|
119
|
+
return this.db.prepare(sql).all(...params);
|
|
120
|
+
}
|
|
121
|
+
/** Run an introspection query and return a single row (or undefined). */
|
|
122
|
+
queryOne(sql, ...params) {
|
|
123
|
+
if (!this.db) {
|
|
124
|
+
throw new Error("Not connected to SQLite database");
|
|
125
|
+
}
|
|
126
|
+
return this.db.prepare(sql).get(...params);
|
|
127
|
+
}
|
|
76
128
|
/**
|
|
77
129
|
* Connect to SQLite database
|
|
78
130
|
* Note: SQLite does not support connection timeouts as it's a local file-based database.
|
|
@@ -82,12 +134,18 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
82
134
|
const parsedConfig = await this.dsnParser.parse(dsn, config);
|
|
83
135
|
this.dbPath = parsedConfig.dbPath;
|
|
84
136
|
try {
|
|
85
|
-
|
|
137
|
+
suppressSqliteExperimentalWarning();
|
|
138
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
139
|
+
const dbOptions = {
|
|
140
|
+
// node:sqlite enables foreign key constraints by default, whereas
|
|
141
|
+
// better-sqlite3 (and SQLite's own default) leaves them off. Preserve
|
|
142
|
+
// the historical behavior; callers can opt in via `PRAGMA foreign_keys = ON`.
|
|
143
|
+
enableForeignKeyConstraints: false
|
|
144
|
+
};
|
|
86
145
|
if (config?.readonly && this.dbPath !== ":memory:") {
|
|
87
|
-
dbOptions.
|
|
146
|
+
dbOptions.readOnly = true;
|
|
88
147
|
}
|
|
89
|
-
this.db = new
|
|
90
|
-
this.db.defaultSafeIntegers(true);
|
|
148
|
+
this.db = new DatabaseSync(this.dbPath, dbOptions);
|
|
91
149
|
if (initScript) {
|
|
92
150
|
this.db.exec(initScript);
|
|
93
151
|
}
|
|
@@ -99,7 +157,8 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
99
157
|
async disconnect() {
|
|
100
158
|
if (this.db) {
|
|
101
159
|
try {
|
|
102
|
-
|
|
160
|
+
const inTransaction = this.db.isTransaction;
|
|
161
|
+
if (!inTransaction) {
|
|
103
162
|
this.db.close();
|
|
104
163
|
} else {
|
|
105
164
|
try {
|
|
@@ -127,13 +186,26 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
127
186
|
throw new Error("Not connected to SQLite database");
|
|
128
187
|
}
|
|
129
188
|
try {
|
|
130
|
-
const rows = this.
|
|
131
|
-
|
|
132
|
-
SELECT name FROM sqlite_master
|
|
189
|
+
const rows = this.queryAll(`
|
|
190
|
+
SELECT name FROM sqlite_master
|
|
133
191
|
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
134
192
|
ORDER BY name
|
|
135
|
-
`
|
|
136
|
-
|
|
193
|
+
`);
|
|
194
|
+
return rows.map((row) => row.name);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async getViews(schema) {
|
|
200
|
+
if (!this.db) {
|
|
201
|
+
throw new Error("Not connected to SQLite database");
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const rows = this.queryAll(`
|
|
205
|
+
SELECT name FROM sqlite_master
|
|
206
|
+
WHERE type='view' AND name NOT LIKE 'sqlite_%'
|
|
207
|
+
ORDER BY name
|
|
208
|
+
`);
|
|
137
209
|
return rows.map((row) => row.name);
|
|
138
210
|
} catch (error) {
|
|
139
211
|
throw error;
|
|
@@ -144,12 +216,13 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
144
216
|
throw new Error("Not connected to SQLite database");
|
|
145
217
|
}
|
|
146
218
|
try {
|
|
147
|
-
const row = this.
|
|
219
|
+
const row = this.queryOne(
|
|
148
220
|
`
|
|
149
|
-
SELECT name FROM sqlite_master
|
|
221
|
+
SELECT name FROM sqlite_master
|
|
150
222
|
WHERE type='table' AND name = ?
|
|
151
|
-
|
|
152
|
-
|
|
223
|
+
`,
|
|
224
|
+
tableName
|
|
225
|
+
);
|
|
153
226
|
return !!row;
|
|
154
227
|
} catch (error) {
|
|
155
228
|
throw error;
|
|
@@ -160,28 +233,33 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
160
233
|
throw new Error("Not connected to SQLite database");
|
|
161
234
|
}
|
|
162
235
|
try {
|
|
163
|
-
const indexInfoRows = this.
|
|
236
|
+
const indexInfoRows = this.queryAll(
|
|
164
237
|
`
|
|
165
|
-
SELECT
|
|
238
|
+
SELECT
|
|
166
239
|
name as index_name,
|
|
167
240
|
0 as is_unique
|
|
168
|
-
FROM sqlite_master
|
|
169
|
-
WHERE type = 'index'
|
|
241
|
+
FROM sqlite_master
|
|
242
|
+
WHERE type = 'index'
|
|
170
243
|
AND tbl_name = ?
|
|
171
|
-
|
|
172
|
-
|
|
244
|
+
`,
|
|
245
|
+
tableName
|
|
246
|
+
);
|
|
173
247
|
const quotedTableName = quoteIdentifier(tableName, "sqlite");
|
|
174
|
-
const indexListRows = this.
|
|
248
|
+
const indexListRows = this.queryAll(
|
|
249
|
+
`PRAGMA index_list(${quotedTableName})`
|
|
250
|
+
);
|
|
175
251
|
const indexUniqueMap = /* @__PURE__ */ new Map();
|
|
176
252
|
for (const indexListRow of indexListRows) {
|
|
177
253
|
indexUniqueMap.set(indexListRow.name, indexListRow.unique === 1);
|
|
178
254
|
}
|
|
179
|
-
const tableInfo = this.
|
|
255
|
+
const tableInfo = this.queryAll(`PRAGMA table_info(${quotedTableName})`);
|
|
180
256
|
const pkColumns = tableInfo.filter((col) => col.pk > 0).map((col) => col.name);
|
|
181
257
|
const results = [];
|
|
182
258
|
for (const indexInfo of indexInfoRows) {
|
|
183
259
|
const quotedIndexName = quoteIdentifier(indexInfo.index_name, "sqlite");
|
|
184
|
-
const indexDetailRows = this.
|
|
260
|
+
const indexDetailRows = this.queryAll(
|
|
261
|
+
`PRAGMA index_info(${quotedIndexName})`
|
|
262
|
+
);
|
|
185
263
|
const columnNames = indexDetailRows.map((row) => row.name);
|
|
186
264
|
results.push({
|
|
187
265
|
index_name: indexInfo.index_name,
|
|
@@ -209,7 +287,7 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
209
287
|
}
|
|
210
288
|
try {
|
|
211
289
|
const quotedTableName = quoteIdentifier(tableName, "sqlite");
|
|
212
|
-
const rows = this.
|
|
290
|
+
const rows = this.queryAll(`PRAGMA table_info(${quotedTableName})`);
|
|
213
291
|
const columns = rows.map((row) => ({
|
|
214
292
|
column_name: row.name,
|
|
215
293
|
data_type: row.type,
|
|
@@ -253,7 +331,7 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
253
331
|
if (isReadStatement) {
|
|
254
332
|
if (parameters && parameters.length > 0) {
|
|
255
333
|
try {
|
|
256
|
-
const rows = this.
|
|
334
|
+
const rows = this.prepare(processedStatement).all(...parameters);
|
|
257
335
|
return { rows, rowCount: rows.length };
|
|
258
336
|
} catch (error) {
|
|
259
337
|
console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
|
|
@@ -262,14 +340,14 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
262
340
|
throw error;
|
|
263
341
|
}
|
|
264
342
|
} else {
|
|
265
|
-
const rows = this.
|
|
343
|
+
const rows = this.prepare(processedStatement).all();
|
|
266
344
|
return { rows, rowCount: rows.length };
|
|
267
345
|
}
|
|
268
346
|
} else {
|
|
269
347
|
let result;
|
|
270
348
|
if (parameters && parameters.length > 0) {
|
|
271
349
|
try {
|
|
272
|
-
result = this.
|
|
350
|
+
result = this.prepare(processedStatement).run(...parameters);
|
|
273
351
|
} catch (error) {
|
|
274
352
|
console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
|
|
275
353
|
console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
|
|
@@ -277,9 +355,9 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
277
355
|
throw error;
|
|
278
356
|
}
|
|
279
357
|
} else {
|
|
280
|
-
result = this.
|
|
358
|
+
result = this.prepare(processedStatement).run();
|
|
281
359
|
}
|
|
282
|
-
return { rows: [], rowCount: result.changes };
|
|
360
|
+
return { rows: [], rowCount: Number(result.changes) };
|
|
283
361
|
}
|
|
284
362
|
} else {
|
|
285
363
|
if (parameters && parameters.length > 0) {
|
|
@@ -297,13 +375,13 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
297
375
|
}
|
|
298
376
|
let totalChanges = 0;
|
|
299
377
|
for (const statement of writeStatements) {
|
|
300
|
-
const result = this.
|
|
301
|
-
totalChanges += result.changes;
|
|
378
|
+
const result = this.prepare(statement).run();
|
|
379
|
+
totalChanges += Number(result.changes);
|
|
302
380
|
}
|
|
303
381
|
let allRows = [];
|
|
304
382
|
for (let statement of readStatements) {
|
|
305
383
|
statement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
|
|
306
|
-
const result = this.
|
|
384
|
+
const result = this.prepare(statement).all();
|
|
307
385
|
allRows.push(...result);
|
|
308
386
|
}
|
|
309
387
|
return { rows: allRows, rowCount: totalChanges + allRows.length };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isDriverNotInstalled
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-WVVMH6FJ.js";
|
|
4
4
|
import {
|
|
5
5
|
SQLRowLimiter
|
|
6
6
|
} from "./chunk-BRXZ5ZQB.js";
|
|
@@ -192,6 +192,7 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
192
192
|
SELECT TABLE_NAME
|
|
193
193
|
FROM INFORMATION_SCHEMA.TABLES
|
|
194
194
|
WHERE TABLE_SCHEMA = @schema
|
|
195
|
+
AND TABLE_TYPE = 'BASE TABLE'
|
|
195
196
|
ORDER BY TABLE_NAME
|
|
196
197
|
`;
|
|
197
198
|
const result = await request.query(query);
|
|
@@ -200,6 +201,26 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
200
201
|
throw new Error(`Failed to get tables: ${error.message}`);
|
|
201
202
|
}
|
|
202
203
|
}
|
|
204
|
+
async getViews(schema) {
|
|
205
|
+
if (!this.connection) {
|
|
206
|
+
throw new Error("Not connected to SQL Server database");
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const schemaToUse = schema || "dbo";
|
|
210
|
+
const request = this.connection.request().input("schema", sql.VarChar, schemaToUse);
|
|
211
|
+
const query = `
|
|
212
|
+
SELECT TABLE_NAME
|
|
213
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
214
|
+
WHERE TABLE_SCHEMA = @schema
|
|
215
|
+
AND TABLE_TYPE = 'VIEW'
|
|
216
|
+
ORDER BY TABLE_NAME
|
|
217
|
+
`;
|
|
218
|
+
const result = await request.query(query);
|
|
219
|
+
return result.recordset.map((row) => row.TABLE_NAME);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
throw new Error(`Failed to get views: ${error.message}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
203
224
|
async tableExists(tableName, schema) {
|
|
204
225
|
if (!this.connection) {
|
|
205
226
|
throw new Error("Not connected to SQL Server database");
|
|
@@ -442,6 +463,19 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
442
463
|
processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sqlQuery, options.maxRows);
|
|
443
464
|
}
|
|
444
465
|
const request = this.connection.request();
|
|
466
|
+
const messages = [];
|
|
467
|
+
request.on(
|
|
468
|
+
"info",
|
|
469
|
+
(info) => {
|
|
470
|
+
messages.push({
|
|
471
|
+
text: info.message,
|
|
472
|
+
// SQL Server reports severity as a numeric class; info messages are < 10.
|
|
473
|
+
severity: info.class !== void 0 ? String(info.class) : void 0,
|
|
474
|
+
code: info.number,
|
|
475
|
+
line: info.lineNumber
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
);
|
|
445
479
|
if (parameters && parameters.length > 0) {
|
|
446
480
|
parameters.forEach((param, index) => {
|
|
447
481
|
const paramName = `p${index + 1}`;
|
|
@@ -477,7 +511,8 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
477
511
|
}
|
|
478
512
|
return {
|
|
479
513
|
rows: result.recordset || [],
|
|
480
|
-
rowCount: result.rowsAffected[0] || 0
|
|
514
|
+
rowCount: result.rowsAffected[0] || 0,
|
|
515
|
+
...messages.length > 0 ? { messages } : {}
|
|
481
516
|
};
|
|
482
517
|
} catch (error) {
|
|
483
518
|
throw new Error(`Failed to execute query: ${error.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebase/dbhub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"mcpName": "io.github.bytebase/dbhub",
|
|
5
5
|
"description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
|
|
6
6
|
"repository": {
|
|
@@ -34,8 +34,9 @@
|
|
|
34
34
|
"test:build": "node scripts/smoke-test-build.mjs"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
37
|
+
"node": ">=22.5.0"
|
|
38
38
|
},
|
|
39
|
+
"packageManager": "pnpm@10.17.1",
|
|
39
40
|
"keywords": [],
|
|
40
41
|
"author": "",
|
|
41
42
|
"license": "MIT",
|
|
@@ -51,7 +52,6 @@
|
|
|
51
52
|
"optionalDependencies": {
|
|
52
53
|
"@aws-sdk/rds-signer": "^3.1001.0",
|
|
53
54
|
"@azure/identity": "^4.8.0",
|
|
54
|
-
"better-sqlite3": "^11.9.0",
|
|
55
55
|
"mariadb": "^3.4.0",
|
|
56
56
|
"mssql": "^11.0.1",
|
|
57
57
|
"mysql2": "^3.13.0",
|
|
@@ -62,7 +62,6 @@
|
|
|
62
62
|
"@testcontainers/mssqlserver": "^11.0.3",
|
|
63
63
|
"@testcontainers/mysql": "^11.0.3",
|
|
64
64
|
"@testcontainers/postgresql": "^11.0.3",
|
|
65
|
-
"@types/better-sqlite3": "^7.6.12",
|
|
66
65
|
"@types/express": "^4.17.21",
|
|
67
66
|
"@types/mssql": "^9.1.7",
|
|
68
67
|
"@types/node": "^22.13.10",
|