@dbx-app/node-core 0.4.6 → 0.4.8
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/LICENSE +201 -661
- package/README.md +1 -7
- package/dist/backend.d.ts +1 -0
- package/dist/backend.js +3 -2
- package/dist/connections.d.ts +17 -8
- package/dist/connections.js +81 -26
- package/dist/database.d.ts +5 -0
- package/dist/database.js +110 -19
- package/dist/diagnostics.d.ts +2 -2
- package/dist/diagnostics.js +5 -13
- package/dist/schema-context.js +3 -14
- package/dist/web-backend.js +17 -2
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -21,13 +21,7 @@ Other DBX connection types can be routed through DBX Desktop bridge integrations
|
|
|
21
21
|
## Public Modules
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
|
-
import {
|
|
25
|
-
createBackend,
|
|
26
|
-
loadConnections,
|
|
27
|
-
getDbxDiagnostics,
|
|
28
|
-
evaluateSqlSafety,
|
|
29
|
-
buildSchemaContext,
|
|
30
|
-
} from "@dbx-app/node-core";
|
|
24
|
+
import { createBackend, loadConnections, getDbxDiagnostics, evaluateSqlSafety, buildSchemaContext } from "@dbx-app/node-core";
|
|
31
25
|
```
|
|
32
26
|
|
|
33
27
|
The package is intended as a shared implementation layer for official DBX Node packages. Applications should prefer `@dbx-app/cli` for terminal workflows and `@dbx-app/mcp-server` for MCP clients.
|
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
|
-
import { addConnection as desktopAddConnection, findConnection as desktopFindConnection, loadConnections as desktopLoadConnections, removeConnection as desktopRemoveConnection
|
|
2
|
-
import { describeTable as desktopDescribeTable, executeQuery as desktopExecuteQuery, listTables as desktopListTables
|
|
1
|
+
import { addConnection as desktopAddConnection, findConnection as desktopFindConnection, loadConnections as desktopLoadConnections, removeConnection as desktopRemoveConnection } from "./connections.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
|
}
|
package/dist/connections.d.ts
CHANGED
|
@@ -9,14 +9,7 @@ export interface ConnectionConfig {
|
|
|
9
9
|
password: string;
|
|
10
10
|
database?: string;
|
|
11
11
|
url_params?: string;
|
|
12
|
-
|
|
13
|
-
ssh_tunnels?: SshTunnelConfig[];
|
|
14
|
-
proxy_enabled?: boolean;
|
|
15
|
-
proxy_type?: "socks5" | "http";
|
|
16
|
-
proxy_host?: string;
|
|
17
|
-
proxy_port?: number;
|
|
18
|
-
proxy_username?: string;
|
|
19
|
-
proxy_password?: string;
|
|
12
|
+
transport_layers?: TransportLayerConfig[];
|
|
20
13
|
ssl: boolean;
|
|
21
14
|
ca_cert_path?: string;
|
|
22
15
|
oracle_connection_type?: "service_name" | "sid";
|
|
@@ -26,7 +19,13 @@ export interface ConnectionConfig {
|
|
|
26
19
|
redis_sentinel_username?: string;
|
|
27
20
|
redis_sentinel_password?: string;
|
|
28
21
|
redis_sentinel_tls?: boolean;
|
|
22
|
+
read_only?: boolean;
|
|
29
23
|
}
|
|
24
|
+
export type TransportLayerConfig = ({
|
|
25
|
+
type: "ssh";
|
|
26
|
+
} & SshTunnelConfig) | ({
|
|
27
|
+
type: "proxy";
|
|
28
|
+
} & ProxyTunnelConfig);
|
|
30
29
|
export interface SshTunnelConfig {
|
|
31
30
|
id: string;
|
|
32
31
|
name?: string;
|
|
@@ -40,6 +39,16 @@ export interface SshTunnelConfig {
|
|
|
40
39
|
connect_timeout_secs?: number;
|
|
41
40
|
expose_lan?: boolean;
|
|
42
41
|
}
|
|
42
|
+
export interface ProxyTunnelConfig {
|
|
43
|
+
id: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
enabled?: boolean;
|
|
46
|
+
proxy_type?: "socks5" | "http";
|
|
47
|
+
host: string;
|
|
48
|
+
port: number;
|
|
49
|
+
username?: string;
|
|
50
|
+
password?: string;
|
|
51
|
+
}
|
|
43
52
|
export interface ConnectionStoreOptions {
|
|
44
53
|
path?: string;
|
|
45
54
|
}
|
package/dist/connections.js
CHANGED
|
@@ -32,11 +32,69 @@ function openDb(readonly = false, path = defaultDbPath()) {
|
|
|
32
32
|
return new Database(path, { readonly });
|
|
33
33
|
}
|
|
34
34
|
function getSecret(db, connectionId, key) {
|
|
35
|
-
const row = db
|
|
36
|
-
.prepare("SELECT secret FROM connection_secrets WHERE connection_id = ? AND key = ?")
|
|
37
|
-
.get(connectionId, key);
|
|
35
|
+
const row = db.prepare("SELECT secret FROM connection_secrets WHERE connection_id = ? AND key = ?").get(connectionId, key);
|
|
38
36
|
return row?.secret ?? "";
|
|
39
37
|
}
|
|
38
|
+
function transportLayerSecretSegment(index, layer) {
|
|
39
|
+
return layer.id?.trim() || String(index);
|
|
40
|
+
}
|
|
41
|
+
function transportLayerSshPasswordKey(index, layer) {
|
|
42
|
+
return `transport_layers.${transportLayerSecretSegment(index, layer)}.ssh_password`;
|
|
43
|
+
}
|
|
44
|
+
function transportLayerSshKeyPassphraseKey(index, layer) {
|
|
45
|
+
return `transport_layers.${transportLayerSecretSegment(index, layer)}.ssh_key_passphrase`;
|
|
46
|
+
}
|
|
47
|
+
function transportLayerProxyPasswordKey(index, layer) {
|
|
48
|
+
return `transport_layers.${transportLayerSecretSegment(index, layer)}.proxy_password`;
|
|
49
|
+
}
|
|
50
|
+
function normalizeTransportLayers(config) {
|
|
51
|
+
if (Array.isArray(config.transport_layers) && config.transport_layers.length > 0)
|
|
52
|
+
return config.transport_layers;
|
|
53
|
+
const layers = [];
|
|
54
|
+
if (config.ssh_enabled && Array.isArray(config.ssh_tunnels) && config.ssh_tunnels.length > 0) {
|
|
55
|
+
layers.push(...config.ssh_tunnels.map((hop) => ({ type: "ssh", ...hop })));
|
|
56
|
+
}
|
|
57
|
+
else if (config.ssh_enabled && config.ssh_host) {
|
|
58
|
+
layers.push({
|
|
59
|
+
type: "ssh",
|
|
60
|
+
id: "legacy",
|
|
61
|
+
enabled: true,
|
|
62
|
+
host: config.ssh_host,
|
|
63
|
+
port: config.ssh_port || 22,
|
|
64
|
+
user: config.ssh_user || "",
|
|
65
|
+
password: config.ssh_password || "",
|
|
66
|
+
key_path: config.ssh_key_path || "",
|
|
67
|
+
key_passphrase: config.ssh_key_passphrase || "",
|
|
68
|
+
connect_timeout_secs: config.ssh_connect_timeout_secs || 5,
|
|
69
|
+
expose_lan: !!config.ssh_expose_lan,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (config.proxy_enabled && config.proxy_host) {
|
|
73
|
+
layers.push({
|
|
74
|
+
type: "proxy",
|
|
75
|
+
id: "legacy-proxy",
|
|
76
|
+
enabled: true,
|
|
77
|
+
proxy_type: config.proxy_type || "socks5",
|
|
78
|
+
host: config.proxy_host,
|
|
79
|
+
port: config.proxy_port || 1080,
|
|
80
|
+
username: config.proxy_username || "",
|
|
81
|
+
password: config.proxy_password || "",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return layers;
|
|
85
|
+
}
|
|
86
|
+
function hydrateTransportLayerSecrets(db, config, connectionId) {
|
|
87
|
+
config.transport_layers = normalizeTransportLayers(config);
|
|
88
|
+
config.transport_layers.forEach((layer, index) => {
|
|
89
|
+
if (layer.type === "ssh") {
|
|
90
|
+
layer.password ||= getSecret(db, connectionId, transportLayerSshPasswordKey(index, layer)) || (layer.id === "legacy" ? getSecret(db, connectionId, "ssh_password") : getSecret(db, connectionId, `ssh_tunnels.${layer.id || index}.password`));
|
|
91
|
+
layer.key_passphrase ||= getSecret(db, connectionId, transportLayerSshKeyPassphraseKey(index, layer)) || (layer.id === "legacy" ? getSecret(db, connectionId, "ssh_key_passphrase") : getSecret(db, connectionId, `ssh_tunnels.${layer.id || index}.key_passphrase`));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
layer.password ||= getSecret(db, connectionId, transportLayerProxyPasswordKey(index, layer)) || (layer.id === "legacy-proxy" ? getSecret(db, connectionId, "proxy_password") : "");
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
40
98
|
export async function loadConnections(options = {}) {
|
|
41
99
|
const path = options.path ?? defaultDbPath();
|
|
42
100
|
if (!existsSync(path))
|
|
@@ -51,8 +109,7 @@ export async function loadConnections(options = {}) {
|
|
|
51
109
|
config.id = row.id;
|
|
52
110
|
if (!config.password)
|
|
53
111
|
config.password = getSecret(db, row.id, "password");
|
|
54
|
-
|
|
55
|
-
config.proxy_password = getSecret(db, row.id, "proxy_password");
|
|
112
|
+
hydrateTransportLayerSecrets(db, config, row.id);
|
|
56
113
|
if (!config.redis_sentinel_password) {
|
|
57
114
|
config.redis_sentinel_password = getSecret(db, row.id, "redis_sentinel_password");
|
|
58
115
|
}
|
|
@@ -109,9 +166,7 @@ export async function inspectConnectionStore(options = {}) {
|
|
|
109
166
|
return diagnostics;
|
|
110
167
|
}
|
|
111
168
|
function tableExists(db, name) {
|
|
112
|
-
const row = db
|
|
113
|
-
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
114
|
-
.get(name);
|
|
169
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name);
|
|
115
170
|
return !!row;
|
|
116
171
|
}
|
|
117
172
|
export async function findConnection(name) {
|
|
@@ -135,21 +190,11 @@ export async function addConnection(config) {
|
|
|
135
190
|
password: "",
|
|
136
191
|
database: normalized.database ?? null,
|
|
137
192
|
color: null,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
ssh_key_path: "",
|
|
144
|
-
ssh_key_passphrase: "",
|
|
145
|
-
ssh_expose_lan: false,
|
|
146
|
-
ssh_tunnels: normalized.ssh_tunnels ?? [],
|
|
147
|
-
proxy_enabled: normalized.proxy_enabled ?? false,
|
|
148
|
-
proxy_type: normalized.proxy_type ?? "socks5",
|
|
149
|
-
proxy_host: normalized.proxy_host ?? "",
|
|
150
|
-
proxy_port: normalized.proxy_port ?? 1080,
|
|
151
|
-
proxy_username: normalized.proxy_username ?? "",
|
|
152
|
-
proxy_password: "",
|
|
193
|
+
transport_layers: normalizeTransportLayers(normalized).map((layer) => {
|
|
194
|
+
if (layer.type === "ssh")
|
|
195
|
+
return { ...layer, password: "", key_passphrase: "" };
|
|
196
|
+
return { ...layer, password: "" };
|
|
197
|
+
}),
|
|
153
198
|
ssl: normalized.ssl ?? false,
|
|
154
199
|
sysdba: false,
|
|
155
200
|
oracle_connection_type: normalized.oracle_connection_type ?? null,
|
|
@@ -167,9 +212,19 @@ export async function addConnection(config) {
|
|
|
167
212
|
if (normalized.password) {
|
|
168
213
|
db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "password", normalized.password);
|
|
169
214
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
215
|
+
normalizeTransportLayers(normalized).forEach((layer, index) => {
|
|
216
|
+
if (layer.type === "ssh") {
|
|
217
|
+
if (layer.password) {
|
|
218
|
+
db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, transportLayerSshPasswordKey(index, layer), layer.password);
|
|
219
|
+
}
|
|
220
|
+
if (layer.key_passphrase) {
|
|
221
|
+
db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, transportLayerSshKeyPassphraseKey(index, layer), layer.key_passphrase);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (layer.password) {
|
|
225
|
+
db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, transportLayerProxyPasswordKey(index, layer), layer.password);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
173
228
|
if (normalized.redis_sentinel_password) {
|
|
174
229
|
db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "redis_sentinel_password", normalized.redis_sentinel_password);
|
|
175
230
|
}
|
package/dist/database.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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 ((
|
|
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 (
|
|
124
|
-
const token = Buffer.from(`${
|
|
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 = !!(
|
|
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(
|
|
158
|
-
const pass = Buffer.from(
|
|
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) {
|
|
@@ -244,7 +274,9 @@ function resolveTimeoutMs(options) {
|
|
|
244
274
|
function convertBridgeQueryResult(result, options) {
|
|
245
275
|
const rows = result.rows.slice(0, resolveMaxRows(options)).map((row) => {
|
|
246
276
|
const obj = {};
|
|
247
|
-
result.columns.forEach((col, i) => {
|
|
277
|
+
result.columns.forEach((col, i) => {
|
|
278
|
+
obj[col] = row[i];
|
|
279
|
+
});
|
|
248
280
|
return obj;
|
|
249
281
|
});
|
|
250
282
|
return { columns: result.columns, rows, row_count: rows.length };
|
|
@@ -292,6 +324,8 @@ async function mysqlQuery(config, sql, params, options) {
|
|
|
292
324
|
async function query(config, sql, params, options) {
|
|
293
325
|
if (config.db_type === "sqlite")
|
|
294
326
|
return sqliteQuery(config, sql, options);
|
|
327
|
+
if (config.db_type === "rqlite")
|
|
328
|
+
return rqliteQuery(config, sql, options);
|
|
295
329
|
if (isMysqlType(config.db_type))
|
|
296
330
|
return mysqlQuery(config, sql, params, options);
|
|
297
331
|
return pgQuery(config, sql, params, options);
|
|
@@ -324,6 +358,48 @@ function sqliteQuery(config, sql, options) {
|
|
|
324
358
|
db.close();
|
|
325
359
|
}
|
|
326
360
|
}
|
|
361
|
+
async function rqliteQuery(config, sql, options) {
|
|
362
|
+
const isReader = /^\s*(?:--[^\n]*\n|\s|\/\*[\s\S]*?\*\/)*(select|pragma|explain|with)\b/i.test(sql);
|
|
363
|
+
const endpoint = isReader ? "/db/query" : "/db/execute";
|
|
364
|
+
const result = await rqliteRequest(config, endpoint, sql);
|
|
365
|
+
if (isReader) {
|
|
366
|
+
const columns = result.columns ?? [];
|
|
367
|
+
const rows = (result.values ?? []).slice(0, resolveMaxRows(options)).map((row) => {
|
|
368
|
+
const record = {};
|
|
369
|
+
columns.forEach((column, index) => {
|
|
370
|
+
record[column] = row[index];
|
|
371
|
+
});
|
|
372
|
+
return record;
|
|
373
|
+
});
|
|
374
|
+
return { columns, rows, row_count: rows.length };
|
|
375
|
+
}
|
|
376
|
+
return { columns: [], rows: [], row_count: result.rows_affected ?? 0 };
|
|
377
|
+
}
|
|
378
|
+
async function rqliteRequest(config, endpoint, sql) {
|
|
379
|
+
const { host, port } = await connectionEndpoint(config);
|
|
380
|
+
const scheme = config.ssl ? "https" : "http";
|
|
381
|
+
const params = (config.url_params || "").trim().replace(/^\?/, "");
|
|
382
|
+
const url = `${scheme}://${host}:${port}${endpoint}${params ? `?${params}` : ""}`;
|
|
383
|
+
const headers = { "content-type": "application/json" };
|
|
384
|
+
if (config.username) {
|
|
385
|
+
headers.authorization = `Basic ${Buffer.from(`${config.username}:${config.password || ""}`).toString("base64")}`;
|
|
386
|
+
}
|
|
387
|
+
const response = await fetch(url, {
|
|
388
|
+
method: "POST",
|
|
389
|
+
headers,
|
|
390
|
+
body: JSON.stringify([sql]),
|
|
391
|
+
});
|
|
392
|
+
const text = await response.text();
|
|
393
|
+
if (!response.ok)
|
|
394
|
+
throw new Error(`rqlite error (${response.status}): ${text}`);
|
|
395
|
+
const payload = JSON.parse(text);
|
|
396
|
+
const result = payload.results?.[0];
|
|
397
|
+
if (!result)
|
|
398
|
+
throw new Error("rqlite returned no result");
|
|
399
|
+
if (result.error)
|
|
400
|
+
throw new Error(`rqlite error: ${result.error}`);
|
|
401
|
+
return result;
|
|
402
|
+
}
|
|
327
403
|
export async function executeQuery(config, sql, options) {
|
|
328
404
|
if (config.db_type === "mongodb") {
|
|
329
405
|
const find = parseMongoFindCommand(sql);
|
|
@@ -344,6 +420,11 @@ export async function executeQuery(config, sql, options) {
|
|
|
344
420
|
const result = await withTimeout(mongoAggregateDocuments(config, aggregate.collection, aggregate.pipeline, resolveMaxRows(options)), resolveTimeoutMs(options));
|
|
345
421
|
return mongoDocumentsToQueryResult(result.documents.slice(0, resolveMaxRows(options)), result.total);
|
|
346
422
|
}
|
|
423
|
+
const getIndexes = parseMongoGetIndexesCommand(sql);
|
|
424
|
+
if (getIndexes) {
|
|
425
|
+
const result = await withTimeout(mongoAggregateDocuments(config, getIndexes.collection, '[{"$indexStats":{}}]', resolveMaxRows(options)), resolveTimeoutMs(options));
|
|
426
|
+
return mongoDocumentsToQueryResult(result.documents.slice(0, resolveMaxRows(options)), result.total);
|
|
427
|
+
}
|
|
347
428
|
const write = parseMongoWriteCommand(sql);
|
|
348
429
|
if (write) {
|
|
349
430
|
const safety = evaluateMongoWriteSafety(write, sqlSafetyFromEnv());
|
|
@@ -352,7 +433,7 @@ export async function executeQuery(config, sql, options) {
|
|
|
352
433
|
const affected = await withTimeout(executeMongoWrite(config, write), resolveTimeoutMs(options));
|
|
353
434
|
return { columns: [], rows: [], row_count: affected };
|
|
354
435
|
}
|
|
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({...})");
|
|
436
|
+
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
437
|
}
|
|
357
438
|
if (isDirectQueryType(config.db_type)) {
|
|
358
439
|
return query(config, sql, undefined, options);
|
|
@@ -373,7 +454,7 @@ export async function listTables(config, schema) {
|
|
|
373
454
|
});
|
|
374
455
|
return collections.map((name) => ({ name, type: "COLLECTION" }));
|
|
375
456
|
}
|
|
376
|
-
if (config.db_type === "sqlite") {
|
|
457
|
+
if (config.db_type === "sqlite" || config.db_type === "rqlite") {
|
|
377
458
|
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
459
|
return result.rows.map((r) => ({ name: String(r.name || ""), type: String(r.type || "table") }));
|
|
379
460
|
}
|
|
@@ -399,7 +480,7 @@ export async function describeTable(config, table, schema) {
|
|
|
399
480
|
const result = await mongoFindDocuments(config, table, 0, 20, "{}");
|
|
400
481
|
return inferMongoColumns(result.documents);
|
|
401
482
|
}
|
|
402
|
-
if (config.db_type === "sqlite") {
|
|
483
|
+
if (config.db_type === "sqlite" || config.db_type === "rqlite") {
|
|
403
484
|
const result = await query(config, `PRAGMA table_info(${quoteSqliteIdentifier(table)})`);
|
|
404
485
|
return result.rows.map((r) => ({
|
|
405
486
|
name: String(r.name || ""),
|
|
@@ -598,6 +679,16 @@ export function parseMongoAggregateCommand(input) {
|
|
|
598
679
|
return null;
|
|
599
680
|
return Array.isArray(JSON.parse(pipeline)) ? { collection: target.collection, pipeline } : null;
|
|
600
681
|
}
|
|
682
|
+
export function parseMongoGetIndexesCommand(input) {
|
|
683
|
+
const source = input.trim().replace(/;$/, "").trim();
|
|
684
|
+
const target = parseCollectionMethodTarget(source, "getIndexes");
|
|
685
|
+
if (!target)
|
|
686
|
+
return null;
|
|
687
|
+
const args = parseMethodArgs(source, target.methodCallIndex);
|
|
688
|
+
if (!args || args.some((arg) => arg.trim()))
|
|
689
|
+
return null;
|
|
690
|
+
return { collection: target.collection };
|
|
691
|
+
}
|
|
601
692
|
export function mongoAggregateWriteStage(pipelineJson) {
|
|
602
693
|
try {
|
|
603
694
|
const pipeline = JSON.parse(pipelineJson);
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -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", "iris", "neo4j", "cassandra", "bigquery", "kylin", "sundb", "xugu", "jdbc", "access"];
|
|
4
|
+
export declare const BRIDGE_REQUIRED_TYPES: readonly ["redis", "mongodb", "duckdb", "clickhouse", "sqlserver", "oracle", "elasticsearch", "etcd", "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;
|
package/dist/diagnostics.js
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
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 = [
|
|
5
|
-
"postgres",
|
|
6
|
-
"redshift",
|
|
7
|
-
"mysql",
|
|
8
|
-
"doris",
|
|
9
|
-
"starrocks",
|
|
10
|
-
"sqlite",
|
|
11
|
-
"gaussdb",
|
|
12
|
-
"opengauss",
|
|
13
|
-
];
|
|
4
|
+
export const DIRECT_QUERY_TYPES = ["postgres", "redshift", "mysql", "doris", "starrocks", "sqlite", "rqlite", "gaussdb", "kwdb", "opengauss"];
|
|
14
5
|
const DIRECT_QUERY_TYPE_SET = new Set(DIRECT_QUERY_TYPES);
|
|
15
6
|
export function isDirectQueryType(dbType) {
|
|
16
7
|
return DIRECT_QUERY_TYPE_SET.has(dbType);
|
|
@@ -23,11 +14,13 @@ export const BRIDGE_REQUIRED_TYPES = [
|
|
|
23
14
|
"sqlserver",
|
|
24
15
|
"oracle",
|
|
25
16
|
"elasticsearch",
|
|
17
|
+
"etcd",
|
|
26
18
|
"dameng",
|
|
27
19
|
"kingbase",
|
|
28
20
|
"highgo",
|
|
29
21
|
"vastbase",
|
|
30
22
|
"goldendb",
|
|
23
|
+
"databend",
|
|
31
24
|
"yashandb",
|
|
32
25
|
"databricks",
|
|
33
26
|
"saphana",
|
|
@@ -38,6 +31,7 @@ export const BRIDGE_REQUIRED_TYPES = [
|
|
|
38
31
|
"oceanbase-oracle",
|
|
39
32
|
"gbase",
|
|
40
33
|
"tdengine",
|
|
34
|
+
"iotdb",
|
|
41
35
|
"h2",
|
|
42
36
|
"snowflake",
|
|
43
37
|
"trino",
|
|
@@ -75,9 +69,7 @@ export async function getDbxDiagnostics() {
|
|
|
75
69
|
loadConnectionsOk: connectionStore.loadConnectionsOk,
|
|
76
70
|
loadedConnectionCount: connectionStore.loadedConnectionCount,
|
|
77
71
|
loadConnectionsError: connectionStore.loadConnectionsError,
|
|
78
|
-
loadConnectionsHint: connectionStore.loadConnectionsError
|
|
79
|
-
? connectionStoreHint(connectionStore.loadConnectionsError)
|
|
80
|
-
: undefined,
|
|
72
|
+
loadConnectionsHint: connectionStore.loadConnectionsError ? connectionStoreHint(connectionStore.loadConnectionsError) : undefined,
|
|
81
73
|
bridgePortFile: portFile,
|
|
82
74
|
bridgePortFileExists,
|
|
83
75
|
bridgeUrl,
|
package/dist/schema-context.js
CHANGED
|
@@ -3,9 +3,7 @@ export async function buildSchemaContext(backend, config, options = {}) {
|
|
|
3
3
|
const maxTables = Math.max(1, Math.min(options.maxTables ?? DEFAULT_MAX_TABLES, 20));
|
|
4
4
|
const availableTables = await backend.listTables(config, options.schema);
|
|
5
5
|
const requested = new Set((options.tables ?? []).map((table) => table.toLowerCase()));
|
|
6
|
-
const selected = requested.size
|
|
7
|
-
? availableTables.filter((table) => requested.has(table.name.toLowerCase()))
|
|
8
|
-
: availableTables.slice(0, maxTables);
|
|
6
|
+
const selected = requested.size ? availableTables.filter((table) => requested.has(table.name.toLowerCase())) : availableTables.slice(0, maxTables);
|
|
9
7
|
const limited = selected.slice(0, maxTables);
|
|
10
8
|
const tables = await Promise.all(limited.map(async (table) => ({
|
|
11
9
|
name: table.name,
|
|
@@ -21,19 +19,10 @@ export async function buildSchemaContext(backend, config, options = {}) {
|
|
|
21
19
|
};
|
|
22
20
|
}
|
|
23
21
|
export function formatSchemaContext(context) {
|
|
24
|
-
const header = [
|
|
25
|
-
`Connection: ${context.connection}`,
|
|
26
|
-
context.database ? `Database: ${context.database}` : "",
|
|
27
|
-
context.schema ? `Schema: ${context.schema}` : "",
|
|
28
|
-
].filter(Boolean);
|
|
22
|
+
const header = [`Connection: ${context.connection}`, context.database ? `Database: ${context.database}` : "", context.schema ? `Schema: ${context.schema}` : ""].filter(Boolean);
|
|
29
23
|
const sections = context.tables.map((table) => {
|
|
30
24
|
const lines = table.columns.map((column) => {
|
|
31
|
-
const parts = [
|
|
32
|
-
column.name,
|
|
33
|
-
column.data_type,
|
|
34
|
-
column.is_nullable ? "NULL" : "NOT NULL",
|
|
35
|
-
column.is_primary_key ? "PK" : "",
|
|
36
|
-
].filter(Boolean);
|
|
25
|
+
const parts = [column.name, column.data_type, column.is_nullable ? "NULL" : "NOT NULL", column.is_primary_key ? "PK" : ""].filter(Boolean);
|
|
37
26
|
return `- ${parts.join(" ")}${column.comment ? ` -- ${column.comment}` : ""}`;
|
|
38
27
|
});
|
|
39
28
|
return [`## ${table.name}`, `Type: ${table.type}`, ...lines].join("\n");
|
package/dist/web-backend.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { evaluateMongoAggregateSafety, evaluateMongoWriteSafety, inferMongoColumns, mongoDocumentsToQueryResult, parseMongoAggregateCommand, parseMongoCountDocumentsCommand, parseMongoFindCommand,
|
|
1
|
+
import { evaluateMongoAggregateSafety, evaluateMongoWriteSafety, inferMongoColumns, mongoDocumentsToQueryResult, parseMongoAggregateCommand, parseMongoCountDocumentsCommand, parseMongoFindCommand, parseMongoGetIndexesCommand, parseMongoWriteCommand } from "./database.js";
|
|
2
2
|
import { sqlSafetyFromEnv } from "./sql-safety.js";
|
|
3
3
|
const baseUrl = process.env.DBX_WEB_URL.replace(/\/+$/, "");
|
|
4
4
|
const password = process.env.DBX_WEB_PASSWORD || "";
|
|
@@ -164,6 +164,21 @@ export async function executeQuery(config, sql, options) {
|
|
|
164
164
|
const result = (await res.json());
|
|
165
165
|
return mongoDocumentsToQueryResult(result.documents.slice(0, options?.maxRows ?? result.documents.length), result.total);
|
|
166
166
|
}
|
|
167
|
+
const getIndexes = parseMongoGetIndexesCommand(sql);
|
|
168
|
+
if (getIndexes) {
|
|
169
|
+
const res = await apiFetch("/api/mongo/aggregate-documents", {
|
|
170
|
+
method: "POST",
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
connectionId: config.id,
|
|
173
|
+
database: config.database || "",
|
|
174
|
+
collection: getIndexes.collection,
|
|
175
|
+
pipelineJson: '[{"$indexStats":{}}]',
|
|
176
|
+
maxRows: options?.maxRows ?? 100,
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
const result = (await res.json());
|
|
180
|
+
return mongoDocumentsToQueryResult(result.documents.slice(0, options?.maxRows ?? result.documents.length), result.total);
|
|
181
|
+
}
|
|
167
182
|
const write = parseMongoWriteCommand(sql);
|
|
168
183
|
if (write) {
|
|
169
184
|
const safety = evaluateMongoWriteSafety(write, sqlSafetyFromEnv());
|
|
@@ -172,7 +187,7 @@ export async function executeQuery(config, sql, options) {
|
|
|
172
187
|
const affected = await executeMongoWrite(config, write);
|
|
173
188
|
return { columns: [], rows: [], row_count: affected };
|
|
174
189
|
}
|
|
175
|
-
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({...})");
|
|
190
|
+
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({...})");
|
|
176
191
|
}
|
|
177
192
|
const res = await apiFetch("/api/query/execute", {
|
|
178
193
|
method: "POST",
|