dsh-db-tool 0.1.6 → 0.1.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/README.md +1 -1
- package/client/client.js +5 -1
- package/dist/adapters/gaussdb/index.js +14 -2
- package/dist/adapters/sql-shared/pg-like.js +20 -14
- package/dist/http/index.js +2 -0
- package/dist/index.js +3 -1
- package/dist/manager.js +20 -7
- package/dist/script/runner.js +5 -3
- package/dist/script/worker.cjs +3 -3
- package/dist/store/connections.js +27 -7
- package/docs/architecture.md +154 -0
- package/docs/install.md +2 -0
- package/package.json +1 -1
- package/skills/db-admin/SKILL.md +13 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ dsh plugin --profile web add "dsh-db-tool@github:mengqi1436/dsh-db-tool"
|
|
|
36
36
|
dsh plugin --profile web add "link:E:\path\to\dsh-db-tool"
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
GaussDB 官方驱动未发布 npm,需先构建 vendor:`npm run build:gaussdb`(PowerShell)或 `bash scripts/build-gaussdb.sh
|
|
39
|
+
GaussDB 官方驱动未发布 npm,需先构建 vendor:`npm run build:gaussdb`(PowerShell)或 `bash scripts/build-gaussdb.sh`;运行要求 Node ≥ 22.12(产物依赖 p-limit@7 纯 ESM)。oracledb 安装脚本需 `npm approve-scripts oracledb`。详见 [docs/install.md](docs/install.md)。
|
|
40
40
|
|
|
41
41
|
## 安全模型
|
|
42
42
|
|
package/client/client.js
CHANGED
|
@@ -38,6 +38,7 @@ window.__ModuleLoader__.load({
|
|
|
38
38
|
fieldsPort: "端口",
|
|
39
39
|
fieldsUser: "用户名",
|
|
40
40
|
fieldsPassword: "密码",
|
|
41
|
+
passwordSaved: "已保存(留空保持不变)",
|
|
41
42
|
fieldsDatabase: "数据库名",
|
|
42
43
|
ssl: "启用 SSL",
|
|
43
44
|
test: "测试",
|
|
@@ -126,6 +127,7 @@ window.__ModuleLoader__.load({
|
|
|
126
127
|
fieldsPort: "Port",
|
|
127
128
|
fieldsUser: "User",
|
|
128
129
|
fieldsPassword: "Password",
|
|
130
|
+
passwordSaved: "Saved (leave blank to keep)",
|
|
129
131
|
fieldsDatabase: "Database",
|
|
130
132
|
ssl: "Enable SSL",
|
|
131
133
|
test: "Test",
|
|
@@ -486,6 +488,8 @@ window.__ModuleLoader__.load({
|
|
|
486
488
|
}
|
|
487
489
|
function draftBody() {
|
|
488
490
|
const body = { kind: form.kind, ssl: !!form.ssl };
|
|
491
|
+
// 编辑已有连接:透传 connId,服务端测试草稿时拼回已存机密(密码留空/url 未改动语义)
|
|
492
|
+
if (props.initial) body.connId = form.id;
|
|
489
493
|
// 编辑保存语义:url 模式未改动(仍等于回填的脱敏 url)→ 不发 url,保留 secrets 原值
|
|
490
494
|
const origUrl = props.initial && props.initial.mode === "url" ? props.initial.url : undefined;
|
|
491
495
|
if (form.mode === "url") {
|
|
@@ -547,7 +551,7 @@ window.__ModuleLoader__.load({
|
|
|
547
551
|
React.createElement("input", { placeholder: t("fieldsDatabase"), value: form.database, onChange: (e) => patch({ database: e.target.value }) })),
|
|
548
552
|
React.createElement("div", { className: "dbt-row" },
|
|
549
553
|
React.createElement("input", { placeholder: t("fieldsUser"), value: form.user, onChange: (e) => patch({ user: e.target.value }) }),
|
|
550
|
-
React.createElement("input", { type: "password", placeholder: t("fieldsPassword"), value: form.password, onChange: (e) => patch({ password: e.target.value }) })),
|
|
554
|
+
React.createElement("input", { type: "password", placeholder: props.initial && props.initial.hasPassword ? t("passwordSaved") : t("fieldsPassword"), value: form.password, onChange: (e) => patch({ password: e.target.value }) })),
|
|
551
555
|
),
|
|
552
556
|
// 分组 3:操作
|
|
553
557
|
React.createElement(
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GaussDB(openGauss)适配器。
|
|
3
3
|
*
|
|
4
|
-
* ⚠️ 官方驱动 openGauss-connector-nodejs 未发布 npm
|
|
5
|
-
*
|
|
4
|
+
* ⚠️ 官方驱动 openGauss-connector-nodejs 未发布 npm(华为云 HCS 26.861.0 官方文档
|
|
5
|
+
* 应用程序接口清单仅 JDBC/ODBC/libpq/Psycopg/Go/ECPG,无 Node 驱动),Node 接入基于
|
|
6
|
+
* PostgreSQL 协议兼容自行验证。使用 vendor/gaussdb-pg 构建产物(fork 自 pg 8.6.0,
|
|
7
|
+
* API 与 pg 完全兼容),未真机验证。
|
|
8
|
+
*
|
|
9
|
+
* 已按官方文档对齐(证据见 skills/db-admin/SKILL.md GaussDB 节):
|
|
10
|
+
* - 认证:驱动实现 openGauss SHA256 / MD5-SHA256 / SM3 握手(vendor pg-protocol parser.ts:335-357、
|
|
11
|
+
* rfc5802.js:44),对应服务端 password_encryption_type 默认 2 (sha256);
|
|
12
|
+
* - 默认端口 8000(集中式 DN / 分布式 CN)、ro 会话 `SET SESSION CHARACTERISTICS AS
|
|
13
|
+
* TRANSACTION READ ONLY` 两形态官方支持(rf-dist/gaussdb-08-0400、rf-cent/gaussdb-38-0416);
|
|
14
|
+
* - 已知限制:SERIALIZABLE 功能不支持(等价 REPEATABLE READ,与 PG 的 SSI 行为不同);
|
|
15
|
+
* SSL 仅布尔开关(不支持 sslmode require/verify-ca/verify-full 细分);
|
|
16
|
+
* information_schema.tables/columns 集中式有官方视图文档、分布式参考无原生视图文档
|
|
17
|
+
* (仅 M-Compatibility 模式),跨库浏览在分布式实例上以真机行为为准。
|
|
6
18
|
*
|
|
7
19
|
* 构建方式:bash scripts/build-gaussdb.sh(或 npm run build:gaussdb)
|
|
8
20
|
* 核心逻辑与 postgresql 适配器共享(sql-shared/pg-like.ts)。
|
|
@@ -10,9 +10,13 @@ function poolConfig(conn) {
|
|
|
10
10
|
}
|
|
11
11
|
else if (conn.fields) {
|
|
12
12
|
const f = conn.fields;
|
|
13
|
+
// 兜底默认值按 kind 区分(仅表单字段被清空时触发):GaussDB 官方默认端口 8000
|
|
14
|
+
// (华为云 HCS 26.861.0:集中式 DN 与分布式 CN 均为 8000,devg-cent/gaussdb-42-0020、
|
|
15
|
+
// devg-dist/gaussdb-12-0319)、默认管理用户 gaussdb;postgresql 维持社区默认 5432/postgres 不变。
|
|
16
|
+
const gb = conn.meta.kind === 'gaussdb';
|
|
13
17
|
base.host = String(f.host ?? '127.0.0.1');
|
|
14
|
-
base.port = Number(f.port ?? 5432);
|
|
15
|
-
base.user = String(f.user ?? f.username ?? 'postgres');
|
|
18
|
+
base.port = Number(f.port ?? (gb ? 8000 : 5432));
|
|
19
|
+
base.user = String(f.user ?? f.username ?? (gb ? 'gaussdb' : 'postgres'));
|
|
16
20
|
if (f.password != null)
|
|
17
21
|
base.password = String(f.password);
|
|
18
22
|
if (f.database != null)
|
|
@@ -28,20 +32,23 @@ function poolConfig(conn) {
|
|
|
28
32
|
export async function createPgLikeAdapter(kind, Driver, conn, opts) {
|
|
29
33
|
const pool = new Driver.Pool(poolConfig(conn));
|
|
30
34
|
const readOnly = opts?.mode === 'ro';
|
|
31
|
-
//
|
|
35
|
+
// 所有池(主池 + 跨库池)统一走此初始化。
|
|
36
|
+
// pg 约定:release(err) 若无等待者会 emit pool 'error',无监听器会崩进程——
|
|
37
|
+
// 官方 Pool 文档要求必须注册 error 监听(空闲连接因后端故障销毁时经此冒泡),无条件兜底。
|
|
38
|
+
// 服务器级 ro 强制(官方手段):每个新连接会话设为只读事务,仅 ro 模式注册。
|
|
32
39
|
// fail-closed:SET 失败时 release(err) 让驱动销毁该连接、等待的 acquire 收到错误——
|
|
33
40
|
// 会话级只读是 ro 的最后防线,不允许静默降级成可写连接。
|
|
34
|
-
const
|
|
35
|
-
p.on('connect', (c) => {
|
|
36
|
-
void c.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY').catch((err) => {
|
|
37
|
-
c.release(err instanceof Error ? err : new Error(String(err)));
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
// pg 约定:release(err) 若无等待者会 emit pool 'error',不监听会崩进程
|
|
41
|
+
const setupPool = (p) => {
|
|
41
42
|
p.on('error', () => { });
|
|
43
|
+
if (readOnly) {
|
|
44
|
+
p.on('connect', (c) => {
|
|
45
|
+
void c.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY').catch((err) => {
|
|
46
|
+
c.release(err instanceof Error ? err : new Error(String(err)));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
42
50
|
};
|
|
43
|
-
|
|
44
|
-
setupReadOnly(pool);
|
|
51
|
+
setupPool(pool);
|
|
45
52
|
// 事务激活期间所有 query/execute 路由到同一 client
|
|
46
53
|
let txClient = null;
|
|
47
54
|
const run = (sql, params) => txClient ? txClient.query(sql, params) : pool.query(sql, params);
|
|
@@ -67,8 +74,7 @@ export async function createPgLikeAdapter(kind, Driver, conn, opts) {
|
|
|
67
74
|
if (cached)
|
|
68
75
|
return cached;
|
|
69
76
|
const created = new Driver.Pool({ ...poolConfig(conn), database: db });
|
|
70
|
-
|
|
71
|
-
setupReadOnly(created);
|
|
77
|
+
setupPool(created); // 与主池一致:error 兜底 + ro 会话只读
|
|
72
78
|
dbPools.set(db, created);
|
|
73
79
|
return created;
|
|
74
80
|
};
|
package/dist/http/index.js
CHANGED
|
@@ -160,6 +160,8 @@ async function route(req, res, service, path, q, opts) {
|
|
|
160
160
|
...(b['url'] !== undefined ? { url: str(b['url']) } : {}),
|
|
161
161
|
...(b['fields'] !== undefined ? { fields: b['fields'] } : {}),
|
|
162
162
|
...(ssl !== undefined ? { ssl } : {}),
|
|
163
|
+
// 编辑已有连接时透传:服务端拼回已存机密(留空密码/url 未改动的测试语义)
|
|
164
|
+
...(b['connId'] !== undefined ? { connId: optStr(b['connId']) } : {}),
|
|
163
165
|
}));
|
|
164
166
|
}
|
|
165
167
|
const connMatch = /^\/api\/connections\/([^/]+)(\/test)?$/.exec(path);
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,9 @@ action 说明:
|
|
|
30
30
|
- preview:预览行,limit≤50(conn_id, table, database?, limit?)。
|
|
31
31
|
- run_script:子进程沙箱脚本(conn_id, code),60s 超时强杀,permission model 禁文件
|
|
32
32
|
访问 + 新 vm realm 双层隔离,仅注入受限 db.query/db.execute 句柄(经完整
|
|
33
|
-
guard/审计链路),rw
|
|
33
|
+
guard/审计链路),rw 连接才有写能力。句柄签名 db.query(sql, params?, database?):
|
|
34
|
+
第三参为跨库路由目标(pg/gaussdb 等按库路由,同 Navicat 式),多库统计在脚本内
|
|
35
|
+
循环逐库调用即可一次完成。`;
|
|
34
36
|
export function apply(ctx) {
|
|
35
37
|
const log = ctx.logger ?? { info: () => { }, error: () => { } };
|
|
36
38
|
const store = new DbToolStore();
|
package/dist/manager.js
CHANGED
|
@@ -75,15 +75,28 @@ export class DbToolService {
|
|
|
75
75
|
/**
|
|
76
76
|
* 测试未保存的连接草稿(侧边栏「保存前测试」)。不落库、不写审计,
|
|
77
77
|
* 一次性适配器用完即关。url/fields 校验交给适配器层(与保存后测试同口径)。
|
|
78
|
+
* 编辑已有连接时传 connId:密码留空/url 未改动时自动拼回已存机密
|
|
79
|
+
* (仅注入本次测试,绝不回传客户端)。
|
|
78
80
|
*/
|
|
79
81
|
async testDraft(input) {
|
|
80
82
|
const rc = {
|
|
81
|
-
meta: { id: '(draft)', kind: input.kind, name: '(draft)' },
|
|
83
|
+
meta: { id: input.connId ?? '(draft)', kind: input.kind, name: '(draft)' },
|
|
82
84
|
};
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (input.
|
|
86
|
-
|
|
85
|
+
let url = input.url;
|
|
86
|
+
let fields = input.fields !== undefined ? { ...input.fields } : undefined;
|
|
87
|
+
if (input.connId) {
|
|
88
|
+
// 已存连接:编辑时客户端不发旧机密(留空语义),这里从 secrets 拼回
|
|
89
|
+
const sec = this.store.secrets.get(input.connId);
|
|
90
|
+
if (url === undefined && sec?.url !== undefined)
|
|
91
|
+
url = sec.url;
|
|
92
|
+
if (fields !== undefined && sec?.password !== undefined && fields.password === undefined) {
|
|
93
|
+
fields.password = sec.password;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (url !== undefined)
|
|
97
|
+
rc.url = url;
|
|
98
|
+
if (fields !== undefined)
|
|
99
|
+
rc.fields = fields;
|
|
87
100
|
if (input.ssl !== undefined)
|
|
88
101
|
rc.ssl = input.ssl;
|
|
89
102
|
try {
|
|
@@ -236,8 +249,8 @@ export class DbToolService {
|
|
|
236
249
|
try {
|
|
237
250
|
const result = await runScriptInChild({
|
|
238
251
|
code: src,
|
|
239
|
-
dbQuery: async (sql, params) => this.unwrapForScript(await this.query(key, connId, sql, params, challengeId)),
|
|
240
|
-
dbExecute: async (statement, params) => this.unwrapForScript(await this.execute(key, connId, statement, params, challengeId)),
|
|
252
|
+
dbQuery: async (sql, params, database) => this.unwrapForScript(await this.query(key, connId, sql, params, challengeId, database)),
|
|
253
|
+
dbExecute: async (statement, params, database) => this.unwrapForScript(await this.execute(key, connId, statement, params, challengeId, database)),
|
|
241
254
|
onLog: (level, text) => level === 'error' ? console.error('[db-script]', text) : console.log('[db-script]', text),
|
|
242
255
|
onTimeout: () => {
|
|
243
256
|
void sessionAdapter.close().catch(() => { });
|
package/dist/script/runner.js
CHANGED
|
@@ -118,11 +118,13 @@ export async function runScriptInChild(opts) {
|
|
|
118
118
|
});
|
|
119
119
|
});
|
|
120
120
|
async function handleDbCall(method, args) {
|
|
121
|
-
const [statement, params] = args;
|
|
121
|
+
const [statement, params, rawDatabase] = args;
|
|
122
|
+
// IPC JSON 序列化会把 undefined 变 null,归一回 undefined 保持与直连调用语义一致
|
|
123
|
+
const database = rawDatabase ?? undefined;
|
|
122
124
|
if (method === 'query')
|
|
123
|
-
return opts.dbQuery(statement, params);
|
|
125
|
+
return opts.dbQuery(statement, params, database);
|
|
124
126
|
if (method === 'execute')
|
|
125
|
-
return opts.dbExecute(statement, params);
|
|
127
|
+
return opts.dbExecute(statement, params, database);
|
|
126
128
|
throw new DbToolError('INVALID_ARGUMENT', `未知 db 方法: ${method}`);
|
|
127
129
|
}
|
|
128
130
|
function errorOf(e) {
|
package/dist/script/worker.cjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* IPC 协议:
|
|
12
12
|
* 父→worker {type:'run', code}
|
|
13
|
-
* worker→父 {type:'db', reqId, method, args}
|
|
13
|
+
* worker→父 {type:'db', reqId, method, args} // args=[statement, params, database?],database 即跨库路由目标
|
|
14
14
|
* 父→worker {type:'dbResult', reqId, ok, result, error}
|
|
15
15
|
* worker→父 {type:'done', ok, result, error}
|
|
16
16
|
*/
|
|
@@ -28,11 +28,11 @@ async function runScript(code) {
|
|
|
28
28
|
const pending = new Map();
|
|
29
29
|
let reqSeq = 0;
|
|
30
30
|
|
|
31
|
-
const dbCall = (method) => (statement, params) =>
|
|
31
|
+
const dbCall = (method) => (statement, params, database) =>
|
|
32
32
|
new Promise((resolve, reject) => {
|
|
33
33
|
const reqId = ++reqSeq;
|
|
34
34
|
pending.set(reqId, { resolve, reject });
|
|
35
|
-
process.send({ type: 'db', reqId, method, args: [statement, params] });
|
|
35
|
+
process.send({ type: 'db', reqId, method, args: [statement, params, database] });
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
const db = {
|
|
@@ -35,8 +35,9 @@ function splitPassword(fields) {
|
|
|
35
35
|
const { [PASSWORD_KEY]: password, ...clean } = fields;
|
|
36
36
|
return { clean, password: password };
|
|
37
37
|
}
|
|
38
|
-
/** ConnRecord → 用户可见的 ConnectionMeta(契约见 lib/adapters/types.ts
|
|
39
|
-
|
|
38
|
+
/** ConnRecord → 用户可见的 ConnectionMeta(契约见 lib/adapters/types.ts)。
|
|
39
|
+
* hasPassword 只回布尔指示,明文绝不出库。 */
|
|
40
|
+
function toMeta(rec, hasPassword = false) {
|
|
40
41
|
const meta = { id: rec.id, kind: rec.kind };
|
|
41
42
|
if (rec.name !== undefined)
|
|
42
43
|
meta.name = rec.name;
|
|
@@ -47,6 +48,8 @@ function toMeta(rec) {
|
|
|
47
48
|
else {
|
|
48
49
|
meta.mode = 'fields';
|
|
49
50
|
}
|
|
51
|
+
if (hasPassword)
|
|
52
|
+
meta.hasPassword = true;
|
|
50
53
|
if (rec.fields) {
|
|
51
54
|
const { host, port, user, database } = rec.fields;
|
|
52
55
|
if (typeof host === 'string')
|
|
@@ -78,12 +81,16 @@ export class ConnectionStore {
|
|
|
78
81
|
findRec(data, id) {
|
|
79
82
|
return data.connections.find((c) => c.id === id);
|
|
80
83
|
}
|
|
84
|
+
/** 该连接是否已存密码(布尔指示,不读明文) */
|
|
85
|
+
hasPassword(id) {
|
|
86
|
+
return this.secrets.get(id)?.password !== undefined;
|
|
87
|
+
}
|
|
81
88
|
list() {
|
|
82
|
-
return this.load().connections.map(toMeta);
|
|
89
|
+
return this.load().connections.map((r) => toMeta(r, this.hasPassword(r.id)));
|
|
83
90
|
}
|
|
84
91
|
get(id) {
|
|
85
92
|
const rec = this.findRec(this.load(), id);
|
|
86
|
-
return rec ? toMeta(rec) : undefined;
|
|
93
|
+
return rec ? toMeta(rec, this.hasPassword(id)) : undefined;
|
|
87
94
|
}
|
|
88
95
|
/** 新建连接;id 已存在时抛错 */
|
|
89
96
|
create(input) {
|
|
@@ -110,7 +117,7 @@ export class ConnectionStore {
|
|
|
110
117
|
this.secrets.set(input.id, secretPatch);
|
|
111
118
|
data.connections.push(rec);
|
|
112
119
|
this.save(data);
|
|
113
|
-
return toMeta(rec);
|
|
120
|
+
return toMeta(rec, this.hasPassword(input.id));
|
|
114
121
|
}
|
|
115
122
|
/** 部分更新;不存在返回 undefined。url/fields 变更时同步拆分 secrets */
|
|
116
123
|
update(id, patch) {
|
|
@@ -134,11 +141,24 @@ export class ConnectionStore {
|
|
|
134
141
|
}
|
|
135
142
|
if (patch.clearUrl && rec.urlSafe !== undefined) {
|
|
136
143
|
delete rec.urlSafe;
|
|
144
|
+
// 密码迁移:原 url 内嵌密码 → fields 密码(切方式后保持可连,编辑留空即保留)
|
|
145
|
+
const secUrl = this.secrets.get(id)?.url;
|
|
146
|
+
if (secUrl !== undefined) {
|
|
147
|
+
try {
|
|
148
|
+
const u = new URL(secUrl);
|
|
149
|
+
if (u.password && this.secrets.get(id)?.password === undefined) {
|
|
150
|
+
this.secrets.set(id, { password: decodeURIComponent(u.password) });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
/* 非 URL 形态,忽略迁移 */
|
|
155
|
+
}
|
|
156
|
+
}
|
|
137
157
|
// set 为合并写;显式置 undefined 经 JSON 序列化后等效删除该键
|
|
138
158
|
this.secrets.set(id, { url: undefined });
|
|
139
159
|
}
|
|
140
160
|
this.save(data);
|
|
141
|
-
return toMeta(rec);
|
|
161
|
+
return toMeta(rec, this.hasPassword(id));
|
|
142
162
|
}
|
|
143
163
|
/** 删除连接,级联删除 secrets 与该连接的所有项目授权。
|
|
144
164
|
* 顺序按"失败开放"原则:先删权限(grants),再删机密(secrets),最后改
|
|
@@ -162,7 +182,7 @@ export class ConnectionStore {
|
|
|
162
182
|
const rec = this.findRec(this.load(), id);
|
|
163
183
|
if (!rec)
|
|
164
184
|
throw new Error(`连接不存在: ${id}`);
|
|
165
|
-
const meta = toMeta(rec);
|
|
185
|
+
const meta = toMeta(rec, this.secrets.get(id)?.password !== undefined);
|
|
166
186
|
const rc = { meta };
|
|
167
187
|
const sec = this.secrets.get(id);
|
|
168
188
|
if (sec?.url !== undefined)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# dsh-db-tool 项目架构图
|
|
2
|
+
|
|
3
|
+
> 由 cbm(codebase-memory)索引数据绘制:843 节点 / 2702 边,模块分层见各图。
|
|
4
|
+
> 索引项目名:`E-Code-dsh-dsh-db-tool`。
|
|
5
|
+
|
|
6
|
+
## 1. 总体分层架构
|
|
7
|
+
|
|
8
|
+
```mermaid
|
|
9
|
+
flowchart TB
|
|
10
|
+
subgraph UI["客户端层(DSH 侧边栏)"]
|
|
11
|
+
CLIENT["client/client.js<br/>连接管理 · 对象树浏览 · SQL 控制台 · 授权面板"]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
subgraph API["接入层"]
|
|
15
|
+
HTTP["lib/http<br/>HTTP API(13 条路由)"]
|
|
16
|
+
TOOL["lib/index.ts<br/>DatabaseManager 工具定义<br/>list_connections / query / execute /<br/>schema / preview / run_script"]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
subgraph SVC["服务编排层"]
|
|
20
|
+
MGR["lib/manager.ts<br/>ro/rw 项目授权校验 · 适配器生命周期 · 事务编排"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
subgraph SEC["安全层"]
|
|
24
|
+
GUARD["lib/guard<br/>SQL/命令分类 · 危险操作识别 ·<br/>一次性 challenge 确认(5 分钟有效)"]
|
|
25
|
+
STORE["lib/store<br/>connections · grants · secrets(脱敏)·<br/>audit 审计 · normalize · io"]
|
|
26
|
+
SCRIPT["lib/script<br/>runner.ts + worker.cjs<br/>子进程沙箱(vm realm · 禁文件 · 60s 超时)"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
subgraph ADPT["适配器层 lib/adapters(8 库)"]
|
|
30
|
+
REG["index.ts<br/>kind → 工厂注册表 + driverMissingError"]
|
|
31
|
+
SQLSHARED["sql-shared/<br/>common.ts(标识符/分页/规范化)<br/>pg-like.ts(PostgreSQL + GaussDB 共享)"]
|
|
32
|
+
A1["mysql"]
|
|
33
|
+
A2["postgresql"]
|
|
34
|
+
A3["gaussdb<br/>vendor/gaussdb-pg 驱动加载"]
|
|
35
|
+
A4["sqlite"]
|
|
36
|
+
A5["redis"]
|
|
37
|
+
A6["mongodb"]
|
|
38
|
+
A7["oracle"]
|
|
39
|
+
A8["dmdb"]
|
|
40
|
+
TYPES["types.ts<br/>DatabaseAdapter 契约"]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
subgraph DRV["驱动层"]
|
|
44
|
+
D1["pg / mysql2 / sqlite3 / redis<br/>mongodb / oracledb / dmdb"]
|
|
45
|
+
D2["vendor/gaussdb-pg<br/>openGauss-connector-nodejs 构建产物"]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
CLIENT -->|HTTP| HTTP
|
|
49
|
+
CLIENT -.->|注入| TOOL
|
|
50
|
+
TOOL --> MGR
|
|
51
|
+
MGR --> GUARD
|
|
52
|
+
MGR --> STORE
|
|
53
|
+
MGR --> SCRIPT
|
|
54
|
+
MGR --> REG
|
|
55
|
+
REG --> SQLSHARED
|
|
56
|
+
REG --> A1 & A4 & A5 & A6 & A7 & A8
|
|
57
|
+
A2 --> SQLSHARED
|
|
58
|
+
A3 --> SQLSHARED
|
|
59
|
+
SQLSHARED --> TYPES
|
|
60
|
+
A1 & A2 & A3 & A4 & A5 & A6 & A7 & A8 -.-> DRV
|
|
61
|
+
SCRIPT -.->|db.query / db.execute 经完整 guard 链路| MGR
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 2. 请求时序(query 与危险操作确认流)
|
|
65
|
+
|
|
66
|
+
```mermaid
|
|
67
|
+
sequenceDiagram
|
|
68
|
+
autonumber
|
|
69
|
+
participant U as 用户(聊天)
|
|
70
|
+
participant T as DatabaseManager 工具
|
|
71
|
+
participant M as lib/manager.ts
|
|
72
|
+
participant G as lib/guard
|
|
73
|
+
participant S as lib/store
|
|
74
|
+
participant A as 适配器(8 库)
|
|
75
|
+
participant DB as 数据库
|
|
76
|
+
|
|
77
|
+
U->>T: query/execute(conn_id, sql)
|
|
78
|
+
T->>S: 授权校验(项目路径 → 连接 → ro/rw)
|
|
79
|
+
alt 未授权
|
|
80
|
+
S-->>T: UNAUTHORIZED_PROJECT
|
|
81
|
+
else ro 授权 + 写操作
|
|
82
|
+
S-->>T: READ_ONLY(拒绝)
|
|
83
|
+
else 通过
|
|
84
|
+
T->>G: 语句分类
|
|
85
|
+
alt 危险操作(DDL/DML/维护命令)
|
|
86
|
+
G-->>T: NEEDS_CONFIRMATION + challengeId(一次性,5 分钟)
|
|
87
|
+
T-->>U: 说明语句与风险,等待明确同意
|
|
88
|
+
U->>T: 携同一 challengeId 重试
|
|
89
|
+
T->>G: 校验 challenge(绑定语句 hash)
|
|
90
|
+
end
|
|
91
|
+
T->>M: 执行
|
|
92
|
+
M->>A: query/execute(可选 database 跨库路由,pg/gaussdb)
|
|
93
|
+
A->>DB: 参数化 SQL(ro 会话级 READ ONLY 双保险)
|
|
94
|
+
DB-->>A: 结果
|
|
95
|
+
A-->>T: 规范化结果(DECIMAL→string 等)
|
|
96
|
+
T->>S: 审计记录
|
|
97
|
+
T-->>U: {columns, rows, rowCount} / {affectedRows, message}
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## 3. 适配器体系(8 库 × 共享实现)
|
|
102
|
+
|
|
103
|
+
```mermaid
|
|
104
|
+
flowchart LR
|
|
105
|
+
subgraph 契约
|
|
106
|
+
T["types.ts<br/>DatabaseAdapter / TableInfo / ColumnInfo<br/>DbKind = 8 种"]
|
|
107
|
+
end
|
|
108
|
+
subgraph 工厂注册
|
|
109
|
+
R["adapters/index.ts<br/>kind → lazy import 工厂<br/>缺失驱动 → driverMissingError(附构建提示)"]
|
|
110
|
+
end
|
|
111
|
+
subgraph SQL系共享
|
|
112
|
+
PG["pg-like.ts<br/>PostgreSQL + GaussDB<br/>pool · ro 会话 · 跨库路由 ·<br/>库→schema→表 三层浏览"]
|
|
113
|
+
CM["common.ts<br/>assertIdent · quoteIdent<br/>clampLimit/Offset · normalizeCell"]
|
|
114
|
+
end
|
|
115
|
+
subgraph 独立实现
|
|
116
|
+
MY["mysql"]
|
|
117
|
+
SQ["sqlite"]
|
|
118
|
+
RD["redis(SCAN 禁 KEYS)"]
|
|
119
|
+
MG["mongodb(强制显式 filter)"]
|
|
120
|
+
OR["oracle"]
|
|
121
|
+
DM["dmdb"]
|
|
122
|
+
end
|
|
123
|
+
PG --> CM
|
|
124
|
+
MY & SQ & RD & MG & OR & DM -.-> CM
|
|
125
|
+
R --> PG & MY & SQ & RD & MG & OR & DM
|
|
126
|
+
PG -.->|"gaussdb 差异仅驱动加载<br/>createRequire vendor/gaussdb-pg"| G["gaussdb/index.ts"]
|
|
127
|
+
T --- R
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## 4. 模块分层职责(cbm layers 数据)
|
|
131
|
+
|
|
132
|
+
| 层 | 模块 | 特征 |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| entry | lib、script、e2e-mock | 仅出边(调用下游) |
|
|
135
|
+
| api | http | 含 13 条 HTTP 路由定义 |
|
|
136
|
+
| core | adapters(4 入 0 出)、store(12 入 0 出) | 高扇入核心,不依赖上层 |
|
|
137
|
+
| internal | manager(7 入 12 出)、tool、http | 编排枢纽 |
|
|
138
|
+
| leaf | guard | 仅入边,纯分类/判定 |
|
|
139
|
+
|
|
140
|
+
## 5. GaussDB 专项链路
|
|
141
|
+
|
|
142
|
+
```mermaid
|
|
143
|
+
flowchart TB
|
|
144
|
+
T["DatabaseManager(query/execute/schema/preview, database=...)"] --> M["manager"]
|
|
145
|
+
M --> A3["gaussdb/index.ts<br/>loadGaussDriver():createRequire<br/>vendor/gaussdb-pg/packages/pg(CJS)"]
|
|
146
|
+
A3 --> PL["sql-shared/pg-like.ts<br/>kind='gaussdb'"]
|
|
147
|
+
PL -->|poolConfig:host/port 8000/user/password/database/ssl| P["PgLikePool"]
|
|
148
|
+
PL -->|database 参数| PF["poolFor(db):按库缓存池(Navicat 式跨库)"]
|
|
149
|
+
PL -->|ro 模式| SRO["每个新连接 SET SESSION CHARACTERISTICS<br/>AS TRANSACTION READ ONLY(fail-closed)"]
|
|
150
|
+
P --> DB[("GaussDB 服务端<br/>openGauss 协议")]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- 驱动构建:`scripts/build-gaussdb.sh` / `.ps1` → 克隆 openGauss-connector-nodejs → `vendor/gaussdb-pg/packages/pg`(gitignore)。
|
|
154
|
+
- 测试:`tests/adapters-sql/gaussdb.test.ts`(驱动加载/工厂,mock 离线)+ `pg-like.test.ts`(pg/gaussdb 共享行为)。
|
package/docs/install.md
CHANGED
|
@@ -43,6 +43,8 @@ rm -rf ../gaussdb-pg && cp -r packages/pg ../gaussdb-pg
|
|
|
43
43
|
|
|
44
44
|
产物为 `vendor/gaussdb-pg`(CJS,结构同 `pg`:`{ Pool, Client, ... }`)。
|
|
45
45
|
|
|
46
|
+
> **Node 版本要求**:≥ 22.12。构建脚本会补装 `p-limit@7`(纯 ESM,源码 package.json 未声明),依赖 Node 原生 require(esm);更旧 Node 加载 GaussDB 驱动会抛 `ERR_REQUIRE_ESM`(其余 7 库不受影响)。
|
|
47
|
+
|
|
46
48
|
## oracledb approve-scripts
|
|
47
49
|
|
|
48
50
|
`oracledb` 安装时需要运行安装脚本编译/下载二进制。仓库 `package.json` 已配置白名单:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-db-tool",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "DSH community plugin: chat-operated multi-database admin tool with sidebar management, project-scoped grants, ro/rw modes and dangerous-operation confirmation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/skills/db-admin/SKILL.md
CHANGED
|
@@ -99,7 +99,7 @@ DatabaseManager({ action: "run_script", conn_id: "mongo1", code: '{"find":"users
|
|
|
99
99
|
|---|---|---|---|---|
|
|
100
100
|
| MySQL | `?` | `information_schema` | `LIMIT n OFFSET m` | |
|
|
101
101
|
| PostgreSQL | `$1, $2…` | `information_schema` / `pg_catalog` | `LIMIT n OFFSET m` | |
|
|
102
|
-
| GaussDB | `$1, $2…` | `information_schema` / `pg_catalog` | `LIMIT n OFFSET m` | PG
|
|
102
|
+
| GaussDB | `$1, $2…` | `information_schema` / `pg_catalog` | `LIMIT n OFFSET m` | PG 系(默认端口 8000,见下方注记) |
|
|
103
103
|
| SQLite | `?` | `sqlite_master` / `PRAGMA` | `LIMIT n OFFSET m` | 库列表固定 `['main']` |
|
|
104
104
|
| Redis | 无(命令数组) | `SCAN`(禁用 `KEYS` 遍历) | `SCAN cursor` | `run_script` 传 `["SCAN","0","MATCH","user:*","COUNT","100"]` |
|
|
105
105
|
| MongoDB | 无(BSON 文档) | `listCollections` | `find().skip().limit()` | `run_script` 传 `{"find":"users","filter":{},"limit":20,"skip":40}` |
|
|
@@ -108,6 +108,18 @@ DatabaseManager({ action: "run_script", conn_id: "mongo1", code: '{"find":"users
|
|
|
108
108
|
|
|
109
109
|
**类型规范化**(工具返回值已自动处理,引用结果时注意):`DECIMAL/NUMERIC` → string(避免精度丢失);`Date/TIMESTAMP` → ISO 8601 string;`LOB/CLOB/BLOB` → string;MongoDB `ObjectId` → string。生成写语句时,DECIMAL 直接传字符串值、日期传 ISO 字符串即可。
|
|
110
110
|
|
|
111
|
+
## GaussDB 官方对齐注记(HCS 26.861.0,集中式/分布式)
|
|
112
|
+
|
|
113
|
+
依据华为云 GaussDB 官方文档(doc.hcs.huawei.com/db/zh-cn,26.861.0 双形态)与 openGauss 官方文档源仓库对齐;未真机验证处以代码内标注为准。
|
|
114
|
+
|
|
115
|
+
- **认证**:驱动实现 openGauss SHA256 / MD5-SHA256 / SM3 握手,匹配服务端默认 `password_encryption_type = 2`(sha256)。连接报 `Invalid username/password, login denied` 时依次排查:① 认证配置(gs_hba.conf)需含 `host all all 0.0.0.0/0 sha256`;② 服务端密码加密方式变更后必须重设密码或新建用户(旧口令仍是旧加密);③ 账号锁定用 `ALTER USER ... ACCOUNT UNLOCK`。
|
|
116
|
+
- **默认端口 8000**(集中式 DN / 分布式 CN,官方 gsql 示例 `-p 8000`);SSL 默认不强制(服务端可 require_ssl 强制)。本工具 SSL 仅开/关,**不支持** sslmode(require/verify-ca/verify-full)证书校验细分。
|
|
117
|
+
- **ro 只读会话**:`SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY` 为官方语法(集中式/分布式均支持,rf-dist/gaussdb-08-0400、rf-cent/gaussdb-38-0416),连接建立时执行一次即对该连接所有后续事务生效。
|
|
118
|
+
- **隔离级别差异**:READ UNCOMMITTED 等价 READ COMMITTED;**SERIALIZABLE 功能不支持,等价 REPEATABLE READ**——与 PG 真 SSI 行为不同,依赖 PG 串行化语义的脚本注意。
|
|
119
|
+
- **元数据**:`pg_database` / `pg_catalog.pg_namespace` 为官方系统表;`information_schema.tables/columns` 集中式有官方视图文档,分布式参考无原生视图(仅 M-Compatibility 模式有),分布式实例上浏览以真机行为为准。
|
|
120
|
+
- **兼容模式**:openGauss 有 sql_compatibility(A/B/C/PG)兼容模式,部分语法语义随模式变化(如 B 模式 SET SESSION TRANSACTION 需 `b_format_behavior_compat_options` 含 set_session_transaction 才生效),本工具按 PG 语义使用。
|
|
121
|
+
- 驱动/默认值差异的代码内证据与证据 URL 见 `lib/adapters/gaussdb/index.ts` 头注释与 `lib/adapters/sql-shared/pg-like.ts`。
|
|
122
|
+
|
|
111
123
|
## 危险操作确认流程(NEEDS_CONFIRMATION)
|
|
112
124
|
|
|
113
125
|
收到形如以下的返回时:
|