@dbx-app/node-core 0.4.3
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 +661 -0
- package/README.md +33 -0
- package/dist/backend.d.ts +12 -0
- package/dist/backend.js +16 -0
- package/dist/bridge.d.ts +9 -0
- package/dist/bridge.js +23 -0
- package/dist/connections.d.ts +43 -0
- package/dist/connections.js +179 -0
- package/dist/database.d.ts +25 -0
- package/dist/database.js +415 -0
- package/dist/diagnostics.d.ts +20 -0
- package/dist/diagnostics.js +79 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/paths.d.ts +3 -0
- package/dist/paths.js +19 -0
- package/dist/schema-context.d.ts +25 -0
- package/dist/schema-context.js +43 -0
- package/dist/sql-safety.d.ts +10 -0
- package/dist/sql-safety.js +103 -0
- package/dist/web-backend.d.ts +9 -0
- package/dist/web-backend.js +115 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# DBX Node Core
|
|
2
|
+
|
|
3
|
+
Shared Node.js runtime utilities for DBX CLI and DBX MCP Server.
|
|
4
|
+
|
|
5
|
+
This package reads DBX Desktop connection storage, redacts connection summaries, builds schema context, applies SQL safety rules, and executes supported direct database queries.
|
|
6
|
+
|
|
7
|
+
## Supported Runtime
|
|
8
|
+
|
|
9
|
+
Requires Node.js 22.13.0 or newer.
|
|
10
|
+
|
|
11
|
+
## Direct Query Support
|
|
12
|
+
|
|
13
|
+
Direct execution currently supports:
|
|
14
|
+
|
|
15
|
+
- PostgreSQL and Redshift
|
|
16
|
+
- MySQL-compatible databases, including MySQL, Doris, and StarRocks
|
|
17
|
+
- SQLite
|
|
18
|
+
|
|
19
|
+
Other DBX connection types can be routed through DBX Desktop bridge integrations used by the CLI and MCP server.
|
|
20
|
+
|
|
21
|
+
## Public Modules
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import {
|
|
25
|
+
createBackend,
|
|
26
|
+
loadConnections,
|
|
27
|
+
getDbxDiagnostics,
|
|
28
|
+
evaluateSqlSafety,
|
|
29
|
+
buildSchemaContext,
|
|
30
|
+
} from "@dbx-app/node-core";
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
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.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ConnectionConfig } from "./connections.js";
|
|
2
|
+
import type { ColumnInfo, QueryOptions, QueryResult, TableInfo } from "./database.js";
|
|
3
|
+
export interface Backend {
|
|
4
|
+
loadConnections(): Promise<ConnectionConfig[]>;
|
|
5
|
+
findConnection(name: string): Promise<ConnectionConfig | undefined>;
|
|
6
|
+
addConnection(config: Omit<ConnectionConfig, "id">): Promise<ConnectionConfig>;
|
|
7
|
+
removeConnection(name: string): Promise<boolean>;
|
|
8
|
+
listTables(config: ConnectionConfig, schema?: string): Promise<TableInfo[]>;
|
|
9
|
+
describeTable(config: ConnectionConfig, table: string, schema?: string): Promise<ColumnInfo[]>;
|
|
10
|
+
executeQuery(config: ConnectionConfig, sql: string, options?: QueryOptions): Promise<QueryResult>;
|
|
11
|
+
}
|
|
12
|
+
export declare function createBackend(env?: NodeJS.ProcessEnv): Promise<Backend>;
|
package/dist/backend.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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";
|
|
3
|
+
export async function createBackend(env = process.env) {
|
|
4
|
+
if (env.DBX_WEB_URL) {
|
|
5
|
+
return await import("./web-backend.js");
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
loadConnections: desktopLoadConnections,
|
|
9
|
+
findConnection: desktopFindConnection,
|
|
10
|
+
addConnection: desktopAddConnection,
|
|
11
|
+
removeConnection: desktopRemoveConnection,
|
|
12
|
+
listTables: desktopListTables,
|
|
13
|
+
describeTable: desktopDescribeTable,
|
|
14
|
+
executeQuery: desktopExecuteQuery,
|
|
15
|
+
};
|
|
16
|
+
}
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function getBridgeUrl(): Promise<string>;
|
|
2
|
+
export declare function postBridge(path: string, body: Record<string, unknown>): Promise<{
|
|
3
|
+
ok: true;
|
|
4
|
+
text: string;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
text: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function notifyReload(): Promise<void>;
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { bridgePortFilePath } from "./paths.js";
|
|
3
|
+
export async function getBridgeUrl() {
|
|
4
|
+
const port = (await readFile(bridgePortFilePath(), "utf-8")).trim();
|
|
5
|
+
return `http://127.0.0.1:${port}`;
|
|
6
|
+
}
|
|
7
|
+
export async function postBridge(path, body) {
|
|
8
|
+
try {
|
|
9
|
+
const bridgeUrl = await getBridgeUrl();
|
|
10
|
+
const res = await fetch(`${bridgeUrl}${path}`, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { "Content-Type": "application/json" },
|
|
13
|
+
body: JSON.stringify(body),
|
|
14
|
+
});
|
|
15
|
+
return { ok: res.ok, text: res.ok ? "" : await res.text() };
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { ok: false, text: "DBX is not running. Please start DBX first." };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function notifyReload() {
|
|
22
|
+
await postBridge("/reload-connections", {});
|
|
23
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface ConnectionConfig {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
db_type: string;
|
|
5
|
+
driver_profile?: string;
|
|
6
|
+
host: string;
|
|
7
|
+
port: number;
|
|
8
|
+
username: string;
|
|
9
|
+
password: string;
|
|
10
|
+
database?: string;
|
|
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;
|
|
19
|
+
ssl: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface ConnectionStoreOptions {
|
|
22
|
+
path?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ConnectionStoreDiagnostics {
|
|
25
|
+
dbPath: string;
|
|
26
|
+
dbPathExists: boolean;
|
|
27
|
+
connectionsTableExists: boolean;
|
|
28
|
+
connectionSecretsTableExists: boolean;
|
|
29
|
+
connectionRowCount: number;
|
|
30
|
+
loadConnectionsOk: boolean;
|
|
31
|
+
loadedConnectionCount: number;
|
|
32
|
+
loadConnectionsError?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare class ConnectionStoreError extends Error {
|
|
35
|
+
readonly code = "CONNECTION_STORE_ERROR";
|
|
36
|
+
constructor(path: string, cause: unknown);
|
|
37
|
+
}
|
|
38
|
+
export declare function canonicalizeConnection(config: ConnectionConfig): ConnectionConfig;
|
|
39
|
+
export declare function loadConnections(options?: ConnectionStoreOptions): Promise<ConnectionConfig[]>;
|
|
40
|
+
export declare function inspectConnectionStore(options?: ConnectionStoreOptions): Promise<ConnectionStoreDiagnostics>;
|
|
41
|
+
export declare function findConnection(name: string): Promise<ConnectionConfig | undefined>;
|
|
42
|
+
export declare function addConnection(config: Omit<ConnectionConfig, "id">): Promise<ConnectionConfig>;
|
|
43
|
+
export declare function removeConnection(name: string): Promise<boolean>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
import { dbPath as defaultDbPath } from "./paths.js";
|
|
5
|
+
export class ConnectionStoreError extends Error {
|
|
6
|
+
code = "CONNECTION_STORE_ERROR";
|
|
7
|
+
constructor(path, cause) {
|
|
8
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
9
|
+
super(`Failed to load DBX connections from ${path}: ${message}`);
|
|
10
|
+
this.name = "ConnectionStoreError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function canonicalizeConnection(config) {
|
|
14
|
+
if (config.db_type === "mysql" && config.driver_profile?.toLowerCase() === "tdengine") {
|
|
15
|
+
return {
|
|
16
|
+
...config,
|
|
17
|
+
db_type: "tdengine",
|
|
18
|
+
driver_profile: "tdengine",
|
|
19
|
+
port: config.port === 0 || config.port === 6030 ? 6041 : config.port,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (config.db_type === "tdengine") {
|
|
23
|
+
return {
|
|
24
|
+
...config,
|
|
25
|
+
driver_profile: "tdengine",
|
|
26
|
+
port: config.port || 6041,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return config;
|
|
30
|
+
}
|
|
31
|
+
function openDb(readonly = false, path = defaultDbPath()) {
|
|
32
|
+
return new Database(path, { readonly });
|
|
33
|
+
}
|
|
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);
|
|
38
|
+
return row?.secret ?? "";
|
|
39
|
+
}
|
|
40
|
+
export async function loadConnections(options = {}) {
|
|
41
|
+
const path = options.path ?? defaultDbPath();
|
|
42
|
+
if (!existsSync(path))
|
|
43
|
+
return [];
|
|
44
|
+
let db;
|
|
45
|
+
try {
|
|
46
|
+
db = openDb(true, path);
|
|
47
|
+
const rows = db.prepare("SELECT id, config_json FROM connections").all();
|
|
48
|
+
const configs = [];
|
|
49
|
+
for (const row of rows) {
|
|
50
|
+
const config = canonicalizeConnection(JSON.parse(row.config_json));
|
|
51
|
+
config.id = row.id;
|
|
52
|
+
if (!config.password)
|
|
53
|
+
config.password = getSecret(db, row.id, "password");
|
|
54
|
+
if (!config.proxy_password)
|
|
55
|
+
config.proxy_password = getSecret(db, row.id, "proxy_password");
|
|
56
|
+
configs.push(config);
|
|
57
|
+
}
|
|
58
|
+
return configs;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw new ConnectionStoreError(path, error);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
db?.close();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function inspectConnectionStore(options = {}) {
|
|
68
|
+
const path = options.path ?? defaultDbPath();
|
|
69
|
+
const diagnostics = {
|
|
70
|
+
dbPath: path,
|
|
71
|
+
dbPathExists: existsSync(path),
|
|
72
|
+
connectionsTableExists: false,
|
|
73
|
+
connectionSecretsTableExists: false,
|
|
74
|
+
connectionRowCount: 0,
|
|
75
|
+
loadConnectionsOk: true,
|
|
76
|
+
loadedConnectionCount: 0,
|
|
77
|
+
};
|
|
78
|
+
if (!diagnostics.dbPathExists)
|
|
79
|
+
return diagnostics;
|
|
80
|
+
let db;
|
|
81
|
+
try {
|
|
82
|
+
db = openDb(true, path);
|
|
83
|
+
diagnostics.connectionsTableExists = tableExists(db, "connections");
|
|
84
|
+
diagnostics.connectionSecretsTableExists = tableExists(db, "connection_secrets");
|
|
85
|
+
if (diagnostics.connectionsTableExists) {
|
|
86
|
+
const row = db.prepare("SELECT COUNT(*) AS count FROM connections").get();
|
|
87
|
+
diagnostics.connectionRowCount = row.count;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
diagnostics.loadConnectionsOk = false;
|
|
92
|
+
diagnostics.loadConnectionsError = error instanceof Error ? error.message : String(error);
|
|
93
|
+
return diagnostics;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
db?.close();
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const connections = await loadConnections({ path });
|
|
100
|
+
diagnostics.loadedConnectionCount = connections.length;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
diagnostics.loadConnectionsOk = false;
|
|
104
|
+
diagnostics.loadConnectionsError = error instanceof Error ? error.message : String(error);
|
|
105
|
+
}
|
|
106
|
+
return diagnostics;
|
|
107
|
+
}
|
|
108
|
+
function tableExists(db, name) {
|
|
109
|
+
const row = db
|
|
110
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
111
|
+
.get(name);
|
|
112
|
+
return !!row;
|
|
113
|
+
}
|
|
114
|
+
export async function findConnection(name) {
|
|
115
|
+
const connections = await loadConnections();
|
|
116
|
+
return connections.find((c) => c.name.toLowerCase() === name.toLowerCase());
|
|
117
|
+
}
|
|
118
|
+
export async function addConnection(config) {
|
|
119
|
+
const id = randomUUID();
|
|
120
|
+
const db = openDb();
|
|
121
|
+
const normalized = canonicalizeConnection({ ...config, id });
|
|
122
|
+
const full = {
|
|
123
|
+
id,
|
|
124
|
+
name: normalized.name,
|
|
125
|
+
db_type: normalized.db_type,
|
|
126
|
+
driver_profile: normalized.driver_profile ?? normalized.db_type,
|
|
127
|
+
driver_label: null,
|
|
128
|
+
url_params: normalized.url_params ?? "",
|
|
129
|
+
host: normalized.host,
|
|
130
|
+
port: normalized.port,
|
|
131
|
+
username: normalized.username,
|
|
132
|
+
password: "",
|
|
133
|
+
database: normalized.database ?? null,
|
|
134
|
+
color: null,
|
|
135
|
+
ssh_enabled: normalized.ssh_enabled ?? false,
|
|
136
|
+
ssh_host: "",
|
|
137
|
+
ssh_port: 22,
|
|
138
|
+
ssh_user: "",
|
|
139
|
+
ssh_password: "",
|
|
140
|
+
ssh_key_path: "",
|
|
141
|
+
ssh_key_passphrase: "",
|
|
142
|
+
ssh_expose_lan: false,
|
|
143
|
+
proxy_enabled: normalized.proxy_enabled ?? false,
|
|
144
|
+
proxy_type: normalized.proxy_type ?? "socks5",
|
|
145
|
+
proxy_host: normalized.proxy_host ?? "",
|
|
146
|
+
proxy_port: normalized.proxy_port ?? 1080,
|
|
147
|
+
proxy_username: normalized.proxy_username ?? "",
|
|
148
|
+
proxy_password: "",
|
|
149
|
+
ssl: normalized.ssl ?? false,
|
|
150
|
+
sysdba: false,
|
|
151
|
+
connection_string: null,
|
|
152
|
+
};
|
|
153
|
+
const configJson = JSON.stringify(full);
|
|
154
|
+
const insert = db.transaction(() => {
|
|
155
|
+
db.prepare("INSERT INTO connections (id, config_json) VALUES (?, ?)").run(id, configJson);
|
|
156
|
+
if (normalized.password) {
|
|
157
|
+
db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "password", normalized.password);
|
|
158
|
+
}
|
|
159
|
+
if (normalized.proxy_password) {
|
|
160
|
+
db.prepare("INSERT INTO connection_secrets (connection_id, key, secret) VALUES (?, ?, ?)").run(id, "proxy_password", normalized.proxy_password);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
insert();
|
|
164
|
+
db.close();
|
|
165
|
+
return normalized;
|
|
166
|
+
}
|
|
167
|
+
export async function removeConnection(name) {
|
|
168
|
+
const connection = await findConnection(name);
|
|
169
|
+
if (!connection)
|
|
170
|
+
return false;
|
|
171
|
+
const db = openDb();
|
|
172
|
+
const remove = db.transaction(() => {
|
|
173
|
+
db.prepare("DELETE FROM connections WHERE id = ?").run(connection.id);
|
|
174
|
+
db.prepare("DELETE FROM connection_secrets WHERE connection_id = ?").run(connection.id);
|
|
175
|
+
});
|
|
176
|
+
remove();
|
|
177
|
+
db.close();
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ConnectionConfig } from "./connections.js";
|
|
2
|
+
export interface TableInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ColumnInfo {
|
|
7
|
+
name: string;
|
|
8
|
+
data_type: string;
|
|
9
|
+
is_nullable: boolean;
|
|
10
|
+
column_default: string | null;
|
|
11
|
+
is_primary_key: boolean;
|
|
12
|
+
comment: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface QueryResult {
|
|
15
|
+
columns: string[];
|
|
16
|
+
rows: Record<string, unknown>[];
|
|
17
|
+
row_count: number;
|
|
18
|
+
}
|
|
19
|
+
export interface QueryOptions {
|
|
20
|
+
maxRows?: number;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function executeQuery(config: ConnectionConfig, sql: string, options?: QueryOptions): Promise<QueryResult>;
|
|
24
|
+
export declare function listTables(config: ConnectionConfig, schema?: string): Promise<TableInfo[]>;
|
|
25
|
+
export declare function describeTable(config: ConnectionConfig, table: string, schema?: string): Promise<ColumnInfo[]>;
|