dsh-db-tool 0.1.3 → 0.1.4

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/client/client.js CHANGED
@@ -66,6 +66,7 @@ window.__ModuleLoader__.load({
66
66
  modeRw: "读写",
67
67
  // 浏览
68
68
  noDatabases: "无可用数据库",
69
+ noSchemas: "无模式",
69
70
  noTables: "无表",
70
71
  loadFailed: "加载失败",
71
72
  structure: "结构",
@@ -150,6 +151,7 @@ window.__ModuleLoader__.load({
150
151
  modeRo: "read-only ro",
151
152
  modeRw: "read-write rw",
152
153
  noDatabases: "No databases available",
154
+ noSchemas: "No schemas",
153
155
  noTables: "No tables",
154
156
  loadFailed: "Failed to load",
155
157
  structure: "Structure",
@@ -708,7 +710,8 @@ window.__ModuleLoader__.load({
708
710
  const [loading, setLoading] = React.useState({});
709
711
  const [error, setError] = React.useState({}); // 树节点加载失败信息(就地显示,可点重试)
710
712
  const [dbs, setDbs] = React.useState({}); // connId -> string[]
711
- const [tablesMap, setTablesMap] = React.useState({}); // "<connId>/<db>" -> TableInfo[]
713
+ const [schemasMap, setSchemasMap] = React.useState({}); // "<connId>/<db>" -> string[](PG/GaussDB 库内 schema 层)
714
+ const [tablesMap, setTablesMap] = React.useState({}); // "<connId>/<db|schema>" -> TableInfo[]
712
715
  const [sel, setSel] = React.useState(null); // {connId, db, table: TableInfo}
713
716
  const [schema, setSchema] = React.useState([]);
714
717
  const [preview, setPreview] = React.useState(null); // QueryResult
@@ -718,7 +721,7 @@ window.__ModuleLoader__.load({
718
721
 
719
722
  // 会话项目切换 / 连接列表变化时清空树缓存,避免陈旧授权下的旧数据
720
723
  React.useEffect(() => {
721
- setOpen({}); setLoading({}); setError({}); setDbs({}); setTablesMap({}); setSel(null); setSchema([]); setPreview(null);
724
+ setOpen({}); setLoading({}); setError({}); setDbs({}); setSchemasMap({}); setTablesMap({}); setSel(null); setSchema([]); setPreview(null);
722
725
  }, [projectPath]);
723
726
 
724
727
  function toggle(key, load) {
@@ -744,11 +747,23 @@ window.__ModuleLoader__.load({
744
747
  setDbs((m) => Object.assign({}, m, { [c.id]: list || [] }));
745
748
  }));
746
749
  }
750
+ // PG/GaussDB 官方层级为 数据库 → 模式(schema) → 表:库节点下先列 schema 再列表
751
+ const HAS_SCHEMAS = { postgresql: true, gaussdb: true };
747
752
  function toggleDb(c, d) {
748
753
  if (!projectPath) return;
754
+ const useSchemas = !!HAS_SCHEMAS[c.kind];
755
+ const what = useSchemas ? "schemas" : "tables";
749
756
  toggle("d:" + c.id + "/" + d, () =>
750
- api("tables" + qs({ project: projectPath, connId: c.id, database: d })).then((list) => {
751
- setTablesMap((m) => Object.assign({}, m, { [c.id + "/" + d]: list || [] }));
757
+ api(what + qs({ project: projectPath, connId: c.id, database: d })).then((list) => {
758
+ const setter = useSchemas ? setSchemasMap : setTablesMap;
759
+ setter((m) => Object.assign({}, m, { [c.id + "/" + d]: list || [] }));
760
+ }));
761
+ }
762
+ function toggleSchema(c, d, s) {
763
+ if (!projectPath) return;
764
+ toggle("s:" + c.id + "/" + d + "/" + s, () =>
765
+ api("tables" + qs({ project: projectPath, connId: c.id, database: s })).then((list) => {
766
+ setTablesMap((m) => Object.assign({}, m, { [c.id + "/" + s]: list || [] }));
752
767
  }));
753
768
  }
754
769
  // 请求序号守卫:快速切换选中表/翻页时,丢弃晚到的旧响应,防止旧数据覆盖新选中项
@@ -795,19 +810,43 @@ window.__ModuleLoader__.load({
795
810
  if (!list) continue;
796
811
  if (list.length === 0) { treeRows.push(treerow(ck + ":e", 1, false, true, t("noDatabases"))); continue; }
797
812
  for (const d of list) {
813
+ const useSchemas = !!HAS_SCHEMAS[c.kind];
798
814
  const dk = "d:" + c.id + "/" + d;
799
815
  const dOpen = !!open[dk];
800
816
  treeRows.push(treerow(dk, 1, dOpen, false, d, () => toggleDb(c, d)));
801
817
  if (!dOpen) continue;
802
- const tkey = c.id + "/" + d;
803
- const tlist = tablesMap[tkey];
804
818
  if (loading[dk]) { treeRows.push(treerow(dk + ":l", 2, false, true, "…")); continue; }
805
819
  if (error[dk]) { treeRows.push(treerow(dk + ":x", 2, false, true, t("loadFailed") + ":" + error[dk], () => retry(dk, () => toggleDb(c, d)))); continue; }
820
+ // PG/GaussDB:库 → 模式 → 表(三层,Navicat 官方层级);schema 节点复用 open/loading/error 状态
821
+ if (useSchemas) {
822
+ const slist = schemasMap[c.id + "/" + d];
823
+ if (!slist) continue;
824
+ if (slist.length === 0) { treeRows.push(treerow(dk + ":e", 2, false, true, t("noSchemas"))); continue; }
825
+ for (const s of slist) {
826
+ const sk = "s:" + c.id + "/" + d + "/" + s;
827
+ const sOpen = !!open[sk];
828
+ treeRows.push(treerow(sk, 2, sOpen, false, s, () => toggleSchema(c, d, s)));
829
+ if (!sOpen) continue;
830
+ const tlist = tablesMap[c.id + "/" + s];
831
+ if (loading[sk]) { treeRows.push(treerow(sk + ":l", 3, false, true, "…")); continue; }
832
+ if (error[sk]) { treeRows.push(treerow(sk + ":x", 3, false, true, t("loadFailed") + ":" + error[sk], () => retry(sk, () => toggleSchema(c, d, s)))); continue; }
833
+ if (!tlist) continue;
834
+ if (tlist.length === 0) { treeRows.push(treerow(sk + ":e", 3, false, true, t("noTables"))); continue; }
835
+ for (const tb of tlist) {
836
+ const active = !!sel && sel.connId === c.id && sel.db === s && sel.table.name === tb.name;
837
+ treeRows.push(treerow("t:" + sk + "/" + tb.name, 3, false, true,
838
+ tb.name + (tb.type && tb.type !== "table" ? " · " + tb.type : ""),
839
+ () => setSel({ connId: c.id, db: s, table: tb }), active));
840
+ }
841
+ }
842
+ continue;
843
+ }
844
+ const tlist = tablesMap[c.id + "/" + d];
806
845
  if (!tlist) continue;
807
846
  if (tlist.length === 0) { treeRows.push(treerow(dk + ":e", 2, false, true, t("noTables"))); continue; }
808
847
  for (const tb of tlist) {
809
848
  const active = !!sel && sel.connId === c.id && sel.db === d && sel.table.name === tb.name;
810
- treeRows.push(treerow("t:" + tkey + "/" + tb.name, 2, false, true,
849
+ treeRows.push(treerow("t:" + c.id + "/" + d + "/" + tb.name, 2, false, true,
811
850
  tb.name + (tb.type && tb.type !== "table" ? " · " + tb.type : ""),
812
851
  () => setSel({ connId: c.id, db: d, table: tb }), active));
813
852
  }
@@ -116,6 +116,14 @@ export async function createPgLikeAdapter(kind, Driver, conn, opts) {
116
116
  const res = await pool.query('SELECT datname FROM pg_database WHERE datistemplate = false AND datallowconn = true ORDER BY datname');
117
117
  return res.rows.map((r) => String(r.datname));
118
118
  }),
119
+ // 库内 schema 清单(Navicat 官方层级:数据库 → 模式 → 表)。
120
+ // PG 连接固定单库,database 参数无法跨库,忽略;pg_* 前缀覆盖 pg_catalog/pg_toast/pg_temp 系。
121
+ listSchemas: () => humanize(`${kind} 列出模式`, async () => {
122
+ const res = await pool.query(`SELECT nspname FROM pg_catalog.pg_namespace
123
+ WHERE nspname NOT LIKE 'pg\\_%' AND nspname <> 'information_schema'
124
+ ORDER BY nspname`);
125
+ return res.rows.map((r) => String(r.nspname));
126
+ }),
119
127
  listTables: (database) => humanize(`${kind} 列出表`, async () => {
120
128
  const schema = database ? assertIdent(database, 'schema') : defaultSchema();
121
129
  const res = await pool.query(`SELECT table_name, table_type FROM information_schema.tables
@@ -210,6 +210,9 @@ async function route(req, res, service, path, q, opts) {
210
210
  if (path === '/api/tables' && req.method === 'GET') {
211
211
  return sendOk(res, await service.tables(projectOf(q), required(q, 'connId'), q.get('database') ?? undefined));
212
212
  }
213
+ if (path === '/api/schemas' && req.method === 'GET') {
214
+ return sendOk(res, await service.schemas(projectOf(q), required(q, 'connId'), q.get('database') ?? undefined));
215
+ }
213
216
  if (path === '/api/schema' && req.method === 'GET') {
214
217
  return sendOk(res, await service.schema(projectOf(q), required(q, 'connId'), required(q, 'table'), q.get('database') ?? undefined));
215
218
  }
package/dist/manager.js CHANGED
@@ -146,6 +146,21 @@ export class DbToolService {
146
146
  throw this.toDriverError(e);
147
147
  }
148
148
  }
149
+ /** 库内 schema 清单(仅 PG/GaussDB 等三层语义适配器实现;ro 即可)。 */
150
+ async schemas(projectPath, connId, database) {
151
+ const { key, adapter } = await this.authorize(projectPath, connId, false);
152
+ if (!adapter.listSchemas)
153
+ return [];
154
+ try {
155
+ const result = await adapter.listSchemas(database);
156
+ this.audit(key, connId, 'schemas', database ?? 'default', 'none', false, true);
157
+ return result;
158
+ }
159
+ catch (e) {
160
+ this.audit(key, connId, 'schemas', database ?? 'default', 'none', false, false, this.errText(e));
161
+ throw this.toDriverError(e);
162
+ }
163
+ }
149
164
  async schema(projectPath, connId, table, database) {
150
165
  const t = assertNonEmpty(table, 'table');
151
166
  const { key, adapter } = await this.authorize(projectPath, connId, false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-db-tool",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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",
@@ -57,6 +57,7 @@ DatabaseManager({ action: "schema", conn_id: "c1", database: "shop", table: "use
57
57
  ```
58
58
 
59
59
  - 表列表返回 `TableInfo[]`:`{name, type?, comment?, database?}`(Redis 的 type 为 hash|list|set|zset|string|stream)。
60
+ - PostgreSQL/GaussDB 为「数据库 → 模式(schema) → 表」三层语义:`database` 参数此时应传 **schema 名**(如 `public`);不确定时先执行 `SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname NOT LIKE 'pg\_%' AND nspname <> 'information_schema'` 列出可用 schema。
60
61
  - 列结构返回 `ColumnInfo[]`:`{name, dataType, nullable, key?, default?, comment?}`。
61
62
 
62
63
  ### 5. preview — 预览表数据