@dbx-app/node-core 0.4.5 → 0.4.7

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/dist/backend.d.ts CHANGED
@@ -8,5 +8,6 @@ export interface Backend {
8
8
  listTables(config: ConnectionConfig, schema?: string): Promise<TableInfo[]>;
9
9
  describeTable(config: ConnectionConfig, table: string, schema?: string): Promise<ColumnInfo[]>;
10
10
  executeQuery(config: ConnectionConfig, sql: string, options?: QueryOptions): Promise<QueryResult>;
11
+ close?(): Promise<void>;
11
12
  }
12
13
  export declare function createBackend(env?: NodeJS.ProcessEnv): Promise<Backend>;
package/dist/backend.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { addConnection as desktopAddConnection, findConnection as desktopFindConnection, loadConnections as desktopLoadConnections, removeConnection as desktopRemoveConnection, } from "./connections.js";
2
- import { describeTable as desktopDescribeTable, executeQuery as desktopExecuteQuery, listTables as desktopListTables, } from "./database.js";
2
+ import { closeDatabaseResources as desktopCloseDatabaseResources, describeTable as desktopDescribeTable, executeQuery as desktopExecuteQuery, listTables as desktopListTables, } from "./database.js";
3
3
  export async function createBackend(env = process.env) {
4
4
  if (env.DBX_WEB_URL) {
5
5
  return await import("./web-backend.js");
@@ -12,5 +12,6 @@ export async function createBackend(env = process.env) {
12
12
  listTables: desktopListTables,
13
13
  describeTable: desktopDescribeTable,
14
14
  executeQuery: desktopExecuteQuery,
15
+ close: desktopCloseDatabaseResources,
15
16
  };
16
17
  }
@@ -9,13 +9,7 @@ export interface ConnectionConfig {
9
9
  password: string;
10
10
  database?: string;
11
11
  url_params?: string;
12
- ssh_enabled: boolean;
13
- proxy_enabled?: boolean;
14
- proxy_type?: "socks5" | "http";
15
- proxy_host?: string;
16
- proxy_port?: number;
17
- proxy_username?: string;
18
- proxy_password?: string;
12
+ transport_layers?: TransportLayerConfig[];
19
13
  ssl: boolean;
20
14
  ca_cert_path?: string;
21
15
  oracle_connection_type?: "service_name" | "sid";
@@ -26,6 +20,34 @@ export interface ConnectionConfig {
26
20
  redis_sentinel_password?: string;
27
21
  redis_sentinel_tls?: boolean;
28
22
  }
23
+ export type TransportLayerConfig = ({
24
+ type: "ssh";
25
+ } & SshTunnelConfig) | ({
26
+ type: "proxy";
27
+ } & ProxyTunnelConfig);
28
+ export interface SshTunnelConfig {
29
+ id: string;
30
+ name?: string;
31
+ enabled?: boolean;
32
+ host: string;
33
+ port: number;
34
+ user: string;
35
+ password?: string;
36
+ key_path?: string;
37
+ key_passphrase?: string;
38
+ connect_timeout_secs?: number;
39
+ expose_lan?: boolean;
40
+ }
41
+ export interface ProxyTunnelConfig {
42
+ id: string;
43
+ name?: string;
44
+ enabled?: boolean;
45
+ proxy_type?: "socks5" | "http";
46
+ host: string;
47
+ port: number;
48
+ username?: string;
49
+ password?: string;
50
+ }
29
51
  export interface ConnectionStoreOptions {
30
52
  path?: string;
31
53
  }
@@ -37,6 +37,74 @@ function getSecret(db, connectionId, key) {
37
37
  .get(connectionId, key);
38
38
  return row?.secret ?? "";
39
39
  }
40
+ function transportLayerSecretSegment(index, layer) {
41
+ return layer.id?.trim() || String(index);
42
+ }
43
+ function transportLayerSshPasswordKey(index, layer) {
44
+ return `transport_layers.${transportLayerSecretSegment(index, layer)}.ssh_password`;
45
+ }
46
+ function transportLayerSshKeyPassphraseKey(index, layer) {
47
+ return `transport_layers.${transportLayerSecretSegment(index, layer)}.ssh_key_passphrase`;
48
+ }
49
+ function transportLayerProxyPasswordKey(index, layer) {
50
+ return `transport_layers.${transportLayerSecretSegment(index, layer)}.proxy_password`;
51
+ }
52
+ function normalizeTransportLayers(config) {
53
+ if (Array.isArray(config.transport_layers) && config.transport_layers.length > 0)
54
+ return config.transport_layers;
55
+ const layers = [];
56
+ if (config.ssh_enabled && Array.isArray(config.ssh_tunnels) && config.ssh_tunnels.length > 0) {
57
+ layers.push(...config.ssh_tunnels.map((hop) => ({ type: "ssh", ...hop })));
58
+ }
59
+ else if (config.ssh_enabled && config.ssh_host) {
60
+ layers.push({
61
+ type: "ssh",
62
+ id: "legacy",
63
+ enabled: true,
64
+ host: config.ssh_host,
65
+ port: config.ssh_port || 22,
66
+ user: config.ssh_user || "",
67
+ password: config.ssh_password || "",
68
+ key_path: config.ssh_key_path || "",
69
+ key_passphrase: config.ssh_key_passphrase || "",
70
+ connect_timeout_secs: config.ssh_connect_timeout_secs || 5,
71
+ expose_lan: !!config.ssh_expose_lan,
72
+ });
73
+ }
74
+ if (config.proxy_enabled && config.proxy_host) {
75
+ layers.push({
76
+ type: "proxy",
77
+ id: "legacy-proxy",
78
+ enabled: true,
79
+ proxy_type: config.proxy_type || "socks5",
80
+ host: config.proxy_host,
81
+ port: config.proxy_port || 1080,
82
+ username: config.proxy_username || "",
83
+ password: config.proxy_password || "",
84
+ });
85
+ }
86
+ return layers;
87
+ }
88
+ function hydrateTransportLayerSecrets(db, config, connectionId) {
89
+ config.transport_layers = normalizeTransportLayers(config);
90
+ config.transport_layers.forEach((layer, index) => {
91
+ if (layer.type === "ssh") {
92
+ layer.password ||=
93
+ getSecret(db, connectionId, transportLayerSshPasswordKey(index, layer)) ||
94
+ (layer.id === "legacy" ? getSecret(db, connectionId, "ssh_password") : getSecret(db, connectionId, `ssh_tunnels.${layer.id || index}.password`));
95
+ layer.key_passphrase ||=
96
+ getSecret(db, connectionId, transportLayerSshKeyPassphraseKey(index, layer)) ||
97
+ (layer.id === "legacy"
98
+ ? getSecret(db, connectionId, "ssh_key_passphrase")
99
+ : getSecret(db, connectionId, `ssh_tunnels.${layer.id || index}.key_passphrase`));
100
+ }
101
+ else {
102
+ layer.password ||=
103
+ getSecret(db, connectionId, transportLayerProxyPasswordKey(index, layer)) ||
104
+ (layer.id === "legacy-proxy" ? getSecret(db, connectionId, "proxy_password") : "");
105
+ }
106
+ });
107
+ }
40
108
  export async function loadConnections(options = {}) {
41
109
  const path = options.path ?? defaultDbPath();
42
110
  if (!existsSync(path))
@@ -51,8 +119,7 @@ export async function loadConnections(options = {}) {
51
119
  config.id = row.id;
52
120
  if (!config.password)
53
121
  config.password = getSecret(db, row.id, "password");
54
- if (!config.proxy_password)
55
- config.proxy_password = getSecret(db, row.id, "proxy_password");
122
+ hydrateTransportLayerSecrets(db, config, row.id);
56
123
  if (!config.redis_sentinel_password) {
57
124
  config.redis_sentinel_password = getSecret(db, row.id, "redis_sentinel_password");
58
125
  }
@@ -135,20 +202,11 @@ export async function addConnection(config) {
135
202
  password: "",
136
203
  database: normalized.database ?? null,
137
204
  color: null,
138
- ssh_enabled: normalized.ssh_enabled ?? false,
139
- ssh_host: "",
140
- ssh_port: 22,
141
- ssh_user: "",
142
- ssh_password: "",
143
- ssh_key_path: "",
144
- ssh_key_passphrase: "",
145
- ssh_expose_lan: false,
146
- proxy_enabled: normalized.proxy_enabled ?? false,
147
- proxy_type: normalized.proxy_type ?? "socks5",
148
- proxy_host: normalized.proxy_host ?? "",
149
- proxy_port: normalized.proxy_port ?? 1080,
150
- proxy_username: normalized.proxy_username ?? "",
151
- proxy_password: "",
205
+ transport_layers: normalizeTransportLayers(normalized).map((layer) => {
206
+ if (layer.type === "ssh")
207
+ return { ...layer, password: "", key_passphrase: "" };
208
+ return { ...layer, password: "" };
209
+ }),
152
210
  ssl: normalized.ssl ?? false,
153
211
  sysdba: false,
154
212
  oracle_connection_type: normalized.oracle_connection_type ?? null,
@@ -166,9 +224,19 @@ export async function addConnection(config) {
166
224
  if (normalized.password) {
167
225
  db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "password", normalized.password);
168
226
  }
169
- if (normalized.proxy_password) {
170
- db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "proxy_password", normalized.proxy_password);
171
- }
227
+ normalizeTransportLayers(normalized).forEach((layer, index) => {
228
+ if (layer.type === "ssh") {
229
+ if (layer.password) {
230
+ db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, transportLayerSshPasswordKey(index, layer), layer.password);
231
+ }
232
+ if (layer.key_passphrase) {
233
+ db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, transportLayerSshKeyPassphraseKey(index, layer), layer.key_passphrase);
234
+ }
235
+ }
236
+ else if (layer.password) {
237
+ db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, transportLayerProxyPasswordKey(index, layer), layer.password);
238
+ }
239
+ });
172
240
  if (normalized.redis_sentinel_password) {
173
241
  db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "redis_sentinel_password", normalized.redis_sentinel_password);
174
242
  }
@@ -20,6 +20,7 @@ export interface QueryOptions {
20
20
  maxRows?: number;
21
21
  timeoutMs?: number;
22
22
  }
23
+ export declare function closeDatabaseResources(): Promise<void>;
23
24
  export declare function executeQuery(config: ConnectionConfig, sql: string, options?: QueryOptions): Promise<QueryResult>;
24
25
  export declare function listTables(config: ConnectionConfig, schema?: string): Promise<TableInfo[]>;
25
26
  export declare function describeTable(config: ConnectionConfig, table: string, schema?: string): Promise<ColumnInfo[]>;
@@ -40,6 +41,9 @@ interface MongoAggregateCommand {
40
41
  collection: string;
41
42
  pipeline: string;
42
43
  }
44
+ interface MongoGetIndexesCommand {
45
+ collection: string;
46
+ }
43
47
  export type MongoWriteCommand = {
44
48
  kind: "insert";
45
49
  collection: string;
@@ -59,6 +63,7 @@ export type MongoWriteCommand = {
59
63
  export declare function parseMongoFindCommand(input: string): MongoFindCommand | null;
60
64
  export declare function parseMongoCountDocumentsCommand(input: string): MongoCountDocumentsCommand | null;
61
65
  export declare function parseMongoAggregateCommand(input: string): MongoAggregateCommand | null;
66
+ export declare function parseMongoGetIndexesCommand(input: string): MongoGetIndexesCommand | null;
62
67
  export declare function mongoAggregateWriteStage(pipelineJson: string): "$out" | "$merge" | null;
63
68
  export declare function parseMongoWriteCommand(input: string): MongoWriteCommand | null;
64
69
  export declare function evaluateMongoWriteSafety(command: MongoWriteCommand, options: {
package/dist/database.js CHANGED
@@ -15,6 +15,7 @@ function poolKey(config) {
15
15
  }
16
16
  function evictPool(key, entry) {
17
17
  pools.delete(key);
18
+ clearTimeout(entry.timer);
18
19
  if (entry.type === "pg") {
19
20
  entry.pool.end().catch(() => { });
20
21
  }
@@ -26,6 +27,26 @@ function resetIdleTimer(key, entry) {
26
27
  clearTimeout(entry.timer);
27
28
  entry.timer = setTimeout(() => evictPool(key, entry), IDLE_TIMEOUT_MS);
28
29
  }
30
+ export async function closeDatabaseResources() {
31
+ const poolEntries = [...pools.entries()];
32
+ pools.clear();
33
+ await Promise.all(poolEntries.map(async ([, entry]) => {
34
+ clearTimeout(entry.timer);
35
+ if (entry.type === "pg") {
36
+ await entry.pool.end().catch(() => { });
37
+ }
38
+ else {
39
+ await entry.pool.end().catch(() => { });
40
+ }
41
+ }));
42
+ const tunnels = [...proxyTunnels.values()];
43
+ proxyTunnels.clear();
44
+ await Promise.all(tunnels.map(({ server, sockets }) => new Promise((resolve) => {
45
+ for (const socket of sockets)
46
+ socket.destroy();
47
+ server.close(() => resolve());
48
+ })));
49
+ }
29
50
  async function getPgPool(config) {
30
51
  const key = poolKey(config);
31
52
  const existing = pools.get(key);
@@ -67,15 +88,24 @@ async function getMysqlPool(config) {
67
88
  resetIdleTimer(key, entry);
68
89
  return pool;
69
90
  }
91
+ function firstProxyLayer(config) {
92
+ return config.transport_layers?.find((layer) => layer.type === "proxy" && layer.enabled !== false && !!layer.host);
93
+ }
70
94
  async function connectionEndpoint(config) {
71
- if (!config.proxy_enabled || !config.proxy_host)
95
+ const proxy = firstProxyLayer(config);
96
+ if (!proxy)
72
97
  return { host: config.host, port: config.port };
73
98
  const existing = proxyTunnels.get(config.id);
74
99
  if (existing)
75
100
  return { host: "127.0.0.1", port: existing.port };
101
+ const sockets = new Set();
76
102
  const server = createServer((inbound) => {
77
- connectViaProxy(config)
103
+ sockets.add(inbound);
104
+ inbound.once("close", () => sockets.delete(inbound));
105
+ connectViaProxy(config, proxy)
78
106
  .then((outbound) => {
107
+ sockets.add(outbound);
108
+ outbound.once("close", () => sockets.delete(outbound));
79
109
  inbound.pipe(outbound);
80
110
  outbound.pipe(inbound);
81
111
  })
@@ -91,7 +121,7 @@ async function connectionEndpoint(config) {
91
121
  reject(new Error("Failed to bind proxy tunnel"));
92
122
  });
93
123
  });
94
- proxyTunnels.set(config.id, { server, port });
124
+ proxyTunnels.set(config.id, { server, port, sockets });
95
125
  return { host: "127.0.0.1", port };
96
126
  }
97
127
  function buildConnectionUrl(config, endpoint) {
@@ -103,25 +133,25 @@ function buildConnectionUrl(config, endpoint) {
103
133
  }
104
134
  return `postgres://${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@${endpoint.host}:${endpoint.port}/${db}${suffix}`;
105
135
  }
106
- function connectViaProxy(config) {
136
+ function connectViaProxy(config, proxy) {
107
137
  return new Promise((resolve, reject) => {
108
- const socket = netConnect(config.proxy_port || 1080, config.proxy_host || "127.0.0.1");
138
+ const socket = netConnect(proxy.port || 1080, proxy.host || "127.0.0.1");
109
139
  socket.once("error", reject);
110
140
  socket.once("connect", () => {
111
- if ((config.proxy_type || "socks5") === "http") {
112
- httpConnect(socket, config, resolve, reject);
141
+ if ((proxy.proxy_type || "socks5") === "http") {
142
+ httpConnect(socket, config, proxy, resolve, reject);
113
143
  }
114
144
  else {
115
- socks5Connect(socket, config, resolve, reject);
145
+ socks5Connect(socket, config, proxy, resolve, reject);
116
146
  }
117
147
  });
118
148
  });
119
149
  }
120
- function httpConnect(socket, config, resolve, reject) {
150
+ function httpConnect(socket, config, proxy, resolve, reject) {
121
151
  const target = `${config.host}:${config.port}`;
122
152
  const lines = [`CONNECT ${target} HTTP/1.1`, `Host: ${target}`];
123
- if (config.proxy_username || config.proxy_password) {
124
- const token = Buffer.from(`${config.proxy_username || ""}:${config.proxy_password || ""}`).toString("base64");
153
+ if (proxy.username || proxy.password) {
154
+ const token = Buffer.from(`${proxy.username || ""}:${proxy.password || ""}`).toString("base64");
125
155
  lines.push(`Proxy-Authorization: Basic ${token}`);
126
156
  }
127
157
  socket.write(`${lines.join("\r\n")}\r\n\r\n`);
@@ -144,8 +174,8 @@ function httpConnect(socket, config, resolve, reject) {
144
174
  resolve(socket);
145
175
  });
146
176
  }
147
- function socks5Connect(socket, config, resolve, reject) {
148
- const wantsAuth = !!(config.proxy_username || config.proxy_password);
177
+ function socks5Connect(socket, config, proxy, resolve, reject) {
178
+ const wantsAuth = !!(proxy.username || proxy.password);
149
179
  socket.write(Buffer.from(wantsAuth ? [0x05, 0x02, 0x00, 0x02] : [0x05, 0x01, 0x00]));
150
180
  socket.once("data", (method) => {
151
181
  if (method.length < 2 || method[0] !== 0x05) {
@@ -154,8 +184,8 @@ function socks5Connect(socket, config, resolve, reject) {
154
184
  return;
155
185
  }
156
186
  if (method[1] === 0x02) {
157
- const user = Buffer.from(config.proxy_username || "");
158
- const pass = Buffer.from(config.proxy_password || "");
187
+ const user = Buffer.from(proxy.username || "");
188
+ const pass = Buffer.from(proxy.password || "");
159
189
  socket.write(Buffer.concat([Buffer.from([0x01, user.length]), user, Buffer.from([pass.length]), pass]));
160
190
  socket.once("data", (auth) => {
161
191
  if (auth.length < 2 || auth[1] !== 0x00) {
@@ -292,6 +322,8 @@ async function mysqlQuery(config, sql, params, options) {
292
322
  async function query(config, sql, params, options) {
293
323
  if (config.db_type === "sqlite")
294
324
  return sqliteQuery(config, sql, options);
325
+ if (config.db_type === "rqlite")
326
+ return rqliteQuery(config, sql, options);
295
327
  if (isMysqlType(config.db_type))
296
328
  return mysqlQuery(config, sql, params, options);
297
329
  return pgQuery(config, sql, params, options);
@@ -324,6 +356,48 @@ function sqliteQuery(config, sql, options) {
324
356
  db.close();
325
357
  }
326
358
  }
359
+ async function rqliteQuery(config, sql, options) {
360
+ const isReader = /^\s*(?:--[^\n]*\n|\s|\/\*[\s\S]*?\*\/)*(select|pragma|explain|with)\b/i.test(sql);
361
+ const endpoint = isReader ? "/db/query" : "/db/execute";
362
+ const result = await rqliteRequest(config, endpoint, sql);
363
+ if (isReader) {
364
+ const columns = result.columns ?? [];
365
+ const rows = (result.values ?? []).slice(0, resolveMaxRows(options)).map((row) => {
366
+ const record = {};
367
+ columns.forEach((column, index) => {
368
+ record[column] = row[index];
369
+ });
370
+ return record;
371
+ });
372
+ return { columns, rows, row_count: rows.length };
373
+ }
374
+ return { columns: [], rows: [], row_count: result.rows_affected ?? 0 };
375
+ }
376
+ async function rqliteRequest(config, endpoint, sql) {
377
+ const { host, port } = await connectionEndpoint(config);
378
+ const scheme = config.ssl ? "https" : "http";
379
+ const params = (config.url_params || "").trim().replace(/^\?/, "");
380
+ const url = `${scheme}://${host}:${port}${endpoint}${params ? `?${params}` : ""}`;
381
+ const headers = { "content-type": "application/json" };
382
+ if (config.username) {
383
+ headers.authorization = `Basic ${Buffer.from(`${config.username}:${config.password || ""}`).toString("base64")}`;
384
+ }
385
+ const response = await fetch(url, {
386
+ method: "POST",
387
+ headers,
388
+ body: JSON.stringify([sql]),
389
+ });
390
+ const text = await response.text();
391
+ if (!response.ok)
392
+ throw new Error(`rqlite error (${response.status}): ${text}`);
393
+ const payload = JSON.parse(text);
394
+ const result = payload.results?.[0];
395
+ if (!result)
396
+ throw new Error("rqlite returned no result");
397
+ if (result.error)
398
+ throw new Error(`rqlite error: ${result.error}`);
399
+ return result;
400
+ }
327
401
  export async function executeQuery(config, sql, options) {
328
402
  if (config.db_type === "mongodb") {
329
403
  const find = parseMongoFindCommand(sql);
@@ -344,6 +418,11 @@ export async function executeQuery(config, sql, options) {
344
418
  const result = await withTimeout(mongoAggregateDocuments(config, aggregate.collection, aggregate.pipeline, resolveMaxRows(options)), resolveTimeoutMs(options));
345
419
  return mongoDocumentsToQueryResult(result.documents.slice(0, resolveMaxRows(options)), result.total);
346
420
  }
421
+ const getIndexes = parseMongoGetIndexesCommand(sql);
422
+ if (getIndexes) {
423
+ const result = await withTimeout(mongoAggregateDocuments(config, getIndexes.collection, '[{"$indexStats":{}}]', resolveMaxRows(options)), resolveTimeoutMs(options));
424
+ return mongoDocumentsToQueryResult(result.documents.slice(0, resolveMaxRows(options)), result.total);
425
+ }
347
426
  const write = parseMongoWriteCommand(sql);
348
427
  if (write) {
349
428
  const safety = evaluateMongoWriteSafety(write, sqlSafetyFromEnv());
@@ -352,7 +431,7 @@ export async function executeQuery(config, sql, options) {
352
431
  const affected = await withTimeout(executeMongoWrite(config, write), resolveTimeoutMs(options));
353
432
  return { columns: [], rows: [], row_count: affected };
354
433
  }
355
- throw new Error("Use MongoDB shell-style commands, for example: db.projects.find({}).limit(100), db.projects.countDocuments({}), db.projects.insertOne({...}), db.projects.updateOne({...}, {$set: {...}}), or db.projects.deleteOne({...})");
434
+ throw new Error("Use MongoDB shell-style commands, for example: db.projects.find({}).limit(100), db.projects.countDocuments({}), db.projects.getIndexes(), db.projects.insertOne({...}), db.projects.updateOne({...}, {$set: {...}}), or db.projects.deleteOne({...})");
356
435
  }
357
436
  if (isDirectQueryType(config.db_type)) {
358
437
  return query(config, sql, undefined, options);
@@ -373,7 +452,7 @@ export async function listTables(config, schema) {
373
452
  });
374
453
  return collections.map((name) => ({ name, type: "COLLECTION" }));
375
454
  }
376
- if (config.db_type === "sqlite") {
455
+ if (config.db_type === "sqlite" || config.db_type === "rqlite") {
377
456
  const result = await query(config, `SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name`);
378
457
  return result.rows.map((r) => ({ name: String(r.name || ""), type: String(r.type || "table") }));
379
458
  }
@@ -399,7 +478,7 @@ export async function describeTable(config, table, schema) {
399
478
  const result = await mongoFindDocuments(config, table, 0, 20, "{}");
400
479
  return inferMongoColumns(result.documents);
401
480
  }
402
- if (config.db_type === "sqlite") {
481
+ if (config.db_type === "sqlite" || config.db_type === "rqlite") {
403
482
  const result = await query(config, `PRAGMA table_info(${quoteSqliteIdentifier(table)})`);
404
483
  return result.rows.map((r) => ({
405
484
  name: String(r.name || ""),
@@ -598,6 +677,16 @@ export function parseMongoAggregateCommand(input) {
598
677
  return null;
599
678
  return Array.isArray(JSON.parse(pipeline)) ? { collection: target.collection, pipeline } : null;
600
679
  }
680
+ export function parseMongoGetIndexesCommand(input) {
681
+ const source = input.trim().replace(/;$/, "").trim();
682
+ const target = parseCollectionMethodTarget(source, "getIndexes");
683
+ if (!target)
684
+ return null;
685
+ const args = parseMethodArgs(source, target.methodCallIndex);
686
+ if (!args || args.some((arg) => arg.trim()))
687
+ return null;
688
+ return { collection: target.collection };
689
+ }
601
690
  export function mongoAggregateWriteStage(pipelineJson) {
602
691
  try {
603
692
  const pipeline = JSON.parse(pipelineJson);
@@ -731,7 +820,7 @@ function readChainedIntegerArgument(chain, method, fallback) {
731
820
  return Number(arg.trim());
732
821
  }
733
822
  function normalizeJsonArgument(arg) {
734
- const value = (arg.trim() || "{}").replace(/ObjectId\s*\(\s*["']([^"']+)["']\s*\)/g, '{"$oid":"$1"}');
823
+ const value = quoteUnquotedObjectKeys(convertSingleQuotedStrings((arg.trim() || "{}").replace(/ObjectId\s*\(\s*["']([^"']+)["']\s*\)/g, '{"$oid":"$1"}')));
735
824
  try {
736
825
  JSON.parse(value);
737
826
  return value;
@@ -740,6 +829,100 @@ function normalizeJsonArgument(arg) {
740
829
  return null;
741
830
  }
742
831
  }
832
+ function convertSingleQuotedStrings(source) {
833
+ let result = "";
834
+ let copiedUntil = 0;
835
+ let quote = null;
836
+ let start = 0;
837
+ let value = "";
838
+ let escaped = false;
839
+ for (let i = 0; i < source.length; i += 1) {
840
+ const char = source[i];
841
+ if (!quote) {
842
+ if (char === "'") {
843
+ quote = char;
844
+ start = i;
845
+ value = "";
846
+ escaped = false;
847
+ }
848
+ else if (char === '"') {
849
+ quote = char;
850
+ }
851
+ continue;
852
+ }
853
+ if (quote === '"') {
854
+ if (escaped)
855
+ escaped = false;
856
+ else if (char === "\\")
857
+ escaped = true;
858
+ else if (char === '"')
859
+ quote = null;
860
+ continue;
861
+ }
862
+ if (escaped) {
863
+ value += char;
864
+ escaped = false;
865
+ }
866
+ else if (char === "\\") {
867
+ escaped = true;
868
+ }
869
+ else if (char === "'") {
870
+ result += source.slice(copiedUntil, start) + JSON.stringify(value);
871
+ copiedUntil = i + 1;
872
+ quote = null;
873
+ }
874
+ else {
875
+ value += char;
876
+ }
877
+ }
878
+ return quote === "'" ? source : result + source.slice(copiedUntil);
879
+ }
880
+ function quoteUnquotedObjectKeys(source) {
881
+ let result = "";
882
+ let quote = null;
883
+ let escaped = false;
884
+ for (let i = 0; i < source.length; i += 1) {
885
+ const char = source[i];
886
+ if (quote) {
887
+ result += char;
888
+ if (escaped)
889
+ escaped = false;
890
+ else if (char === "\\")
891
+ escaped = true;
892
+ else if (char === quote)
893
+ quote = null;
894
+ continue;
895
+ }
896
+ if (char === '"' || char === "'") {
897
+ quote = char;
898
+ result += char;
899
+ continue;
900
+ }
901
+ if (/[A-Za-z_$]/.test(char) && shouldQuoteObjectKey(source, i)) {
902
+ let end = i + 1;
903
+ while (/[\w$]/.test(source[end] || ""))
904
+ end += 1;
905
+ result += `"${source.slice(i, end)}"`;
906
+ i = end - 1;
907
+ continue;
908
+ }
909
+ result += char;
910
+ }
911
+ return result;
912
+ }
913
+ function shouldQuoteObjectKey(source, index) {
914
+ let before = index - 1;
915
+ while (/\s/.test(source[before] || ""))
916
+ before -= 1;
917
+ if (source[before] !== "{" && source[before] !== ",")
918
+ return false;
919
+ let after = index + 1;
920
+ while (/[\w$]/.test(source[after] || ""))
921
+ after += 1;
922
+ while (/\s/.test(source[after] || ""))
923
+ after += 1;
924
+ return source[after] === ":";
925
+ }
743
926
  function isEmptyJsonObject(json) {
744
927
  try {
745
928
  const parsed = JSON.parse(json);
@@ -1,7 +1,7 @@
1
- export declare const DIRECT_QUERY_TYPES: readonly ["postgres", "redshift", "mysql", "doris", "starrocks", "sqlite", "gaussdb", "opengauss"];
1
+ export declare const DIRECT_QUERY_TYPES: readonly ["postgres", "redshift", "mysql", "doris", "starrocks", "sqlite", "rqlite", "gaussdb", "kwdb", "opengauss"];
2
2
  export type DirectQueryType = (typeof DIRECT_QUERY_TYPES)[number];
3
3
  export declare function isDirectQueryType(dbType: string): dbType is DirectQueryType;
4
- export declare const BRIDGE_REQUIRED_TYPES: readonly ["redis", "mongodb", "duckdb", "clickhouse", "sqlserver", "oracle", "elasticsearch", "dameng", "kingbase", "highgo", "vastbase", "goldendb", "yashandb", "databricks", "saphana", "teradata", "vertica", "firebird", "exasol", "oceanbase-oracle", "gbase", "tdengine", "h2", "snowflake", "trino", "hive", "db2", "informix", "neo4j", "cassandra", "bigquery", "kylin", "sundb", "xugu", "jdbc", "access"];
4
+ export declare const BRIDGE_REQUIRED_TYPES: readonly ["redis", "mongodb", "duckdb", "clickhouse", "sqlserver", "oracle", "elasticsearch", "dameng", "kingbase", "highgo", "vastbase", "goldendb", "databend", "yashandb", "databricks", "saphana", "teradata", "vertica", "firebird", "exasol", "oceanbase-oracle", "gbase", "tdengine", "iotdb", "h2", "snowflake", "trino", "hive", "db2", "informix", "iris", "neo4j", "cassandra", "bigquery", "kylin", "sundb", "xugu", "jdbc", "access"];
5
5
  export interface DbxDiagnostics {
6
6
  appDataDir: string;
7
7
  dbPath: string;
@@ -1,7 +1,18 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
2
  import { bridgePortFilePath, dbPath, appDataDir } from "./paths.js";
3
3
  import { inspectConnectionStore } from "./connections.js";
4
- export const DIRECT_QUERY_TYPES = ["postgres", "redshift", "mysql", "doris", "starrocks", "sqlite", "gaussdb", "opengauss"];
4
+ export const DIRECT_QUERY_TYPES = [
5
+ "postgres",
6
+ "redshift",
7
+ "mysql",
8
+ "doris",
9
+ "starrocks",
10
+ "sqlite",
11
+ "rqlite",
12
+ "gaussdb",
13
+ "kwdb",
14
+ "opengauss",
15
+ ];
5
16
  const DIRECT_QUERY_TYPE_SET = new Set(DIRECT_QUERY_TYPES);
6
17
  export function isDirectQueryType(dbType) {
7
18
  return DIRECT_QUERY_TYPE_SET.has(dbType);
@@ -19,6 +30,7 @@ export const BRIDGE_REQUIRED_TYPES = [
19
30
  "highgo",
20
31
  "vastbase",
21
32
  "goldendb",
33
+ "databend",
22
34
  "yashandb",
23
35
  "databricks",
24
36
  "saphana",
@@ -29,12 +41,14 @@ export const BRIDGE_REQUIRED_TYPES = [
29
41
  "oceanbase-oracle",
30
42
  "gbase",
31
43
  "tdengine",
44
+ "iotdb",
32
45
  "h2",
33
46
  "snowflake",
34
47
  "trino",
35
48
  "hive",
36
49
  "db2",
37
50
  "informix",
51
+ "iris",
38
52
  "neo4j",
39
53
  "cassandra",
40
54
  "bigquery",
@@ -1,5 +1,15 @@
1
1
  const READ_KEYWORDS = new Set(["select", "with", "show", "describe", "desc", "explain"]);
2
2
  const DANGEROUS_KEYWORDS = new Set(["drop", "truncate", "alter"]);
3
+ function parseBooleanEnv(value) {
4
+ if (value === undefined)
5
+ return undefined;
6
+ const normalized = value.trim().toLowerCase();
7
+ if (normalized === "1" || normalized === "true")
8
+ return true;
9
+ if (normalized === "0" || normalized === "false")
10
+ return false;
11
+ return undefined;
12
+ }
3
13
  export function evaluateSqlSafety(sql, options = {}) {
4
14
  const statements = splitSqlStatements(sql);
5
15
  if (statements.length === 0)
@@ -33,7 +43,7 @@ function evaluateSingleSqlStatementSafety(sql, options = {}) {
33
43
  if (!options.allowWrites && !READ_KEYWORDS.has(firstKeyword)) {
34
44
  return {
35
45
  allowed: false,
36
- reason: "MCP SQL execution is read-only by default. Set DBX_MCP_ALLOW_WRITES=1 to allow write statements.",
46
+ reason: "MCP SQL execution is read-only for this session. Set DBX_MCP_ALLOW_WRITES=1 to allow write statements.",
37
47
  };
38
48
  }
39
49
  if (options.allowWrites && !options.allowDangerous) {
@@ -47,9 +57,11 @@ function evaluateSingleSqlStatementSafety(sql, options = {}) {
47
57
  return { allowed: true };
48
58
  }
49
59
  export function sqlSafetyFromEnv(env = process.env) {
60
+ const allowWrites = parseBooleanEnv(env.DBX_MCP_ALLOW_WRITES);
61
+ const allowDangerous = parseBooleanEnv(env.DBX_MCP_ALLOW_DANGEROUS_SQL);
50
62
  return {
51
- allowWrites: env.DBX_MCP_ALLOW_WRITES === "1" || env.DBX_MCP_ALLOW_WRITES === "true",
52
- allowDangerous: env.DBX_MCP_ALLOW_DANGEROUS_SQL === "1" || env.DBX_MCP_ALLOW_DANGEROUS_SQL === "true",
63
+ allowWrites: allowWrites ?? true,
64
+ allowDangerous: allowDangerous ?? false,
53
65
  };
54
66
  }
55
67
  export function splitSqlStatements(sql) {