mysql-dashboard 0.1.0 → 0.2.0

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/DEVELOPMENT.md CHANGED
@@ -9,15 +9,76 @@
9
9
  | 文档 | 行 |
10
10
  | mongodb 驱动 | mysql2 (promise) |
11
11
 
12
+ ## 仓库结构(要点)
13
+
14
+ - `server/`:JSON API(`addRoute`),入口 `server/index.ts` 串联注册各模块。
15
+ - `src/`:React 前端,`src/index.tsx` 中配置路由与 Ant Design 主题。
16
+ - `start.mjs`:f2e-server3 命令入口(`mysql-dashboard`),开发态拉取 Tailwind PostCSS,生产态静态资源来自 `output/`。
17
+ - 前端路径别名:`@/*` → `src/*`,`@server/*` → `server/*`(例如页面中复用 `isReadOnlyDatabase`)。
18
+
12
19
  ## 本地开发
13
20
 
14
21
  ```bash
15
22
  npm install
16
- npm run build:server # 生成 lib/index.js
17
- npm run dev # 前端 + API 联调
23
+ npm run build:server # 生成 lib/index.js(dev 脚本会先执行此项)
24
+ npm run dev # 前端 + API 联调,默认端口 7018
18
25
  ```
19
26
 
20
- 服务端入口在 `server/index.ts`,通过 `f2e-server3` 的 `addRoute` 注册 JSON API。连接配置保存在项目目录 `.f2e_cache/mysql-connection.json`。
27
+ ## 前端路由
28
+
29
+ | 路径 | 页面 |
30
+ |------|------|
31
+ | `/` | 连接列表 |
32
+ | `/connection/:connectionId` | 数据库列表 |
33
+ | `/connection/:connectionId/database/:databaseName` | 当前库下表列表与表结构操作 |
34
+ | `/connection/.../collection/:collectionName` | 表数据 + 查询编辑器 |
35
+ | `/help/readme`、`/help/development` | 内嵌 Markdown 帮助 |
36
+
37
+ ## 服务端 API 一览
38
+
39
+ 连接(`server/api/connection.ts`):
40
+
41
+ - `POST /api/connection/list`
42
+ - `POST /api/connection/create`
43
+ - `POST /api/connection/update/:_id`
44
+ - `POST /api/connection/delete/:_id`
45
+ - `POST /api/connection/test`
46
+
47
+ 数据库(`server/api/database.ts`):
48
+
49
+ - `POST /api/database/list`
50
+ - `POST /api/database/create`
51
+ - `POST /api/database/delete`
52
+ - `POST /api/database/rename`(建库 + 逐表 `RENAME TABLE` + 删旧库)
53
+
54
+ 表与行(`server/api/collection.ts`):
55
+
56
+ - `POST /api/collection/list`
57
+ - `POST /api/collection/schema`
58
+ - `POST /api/collection/documents`(分页,`hasPrimaryKey`)
59
+ - `POST /api/collection/create`(可选 `columns` 草稿)
60
+ - `POST /api/collection/rename`
61
+ - `POST /api/collection/delete-collection`
62
+ - `POST /api/collection/insert`
63
+ - `POST /api/collection/update`(依赖主键)
64
+ - `POST /api/collection/delete`(依赖主键)
65
+ - `POST /api/collection/alter-schema`(`addColumns` / `modifyColumns` / `dropColumns`,事务包裹)
66
+
67
+ 即席查询(`server/api/query.ts`):
68
+
69
+ - `POST /api/query/execute`:`operation` 为 `find` | `findOne` | `count` | `aggregate`(受限 SELECT)| `insertOne`
70
+
71
+ ## 主键与行编辑
72
+
73
+ 列表与分页接口为每行附加 `__pk`(主键列名 → 值的映射)。更新 / 删除使用主键定位;无主键表在 API 层直接拒绝更新与删除。`__pk` 及以 `__` 开头的字段在写入时会被剥离,不应作为业务列提交。
74
+
75
+ ## 系统库
76
+
77
+ `server/utils/database.ts` 中 `isReadOnlyDatabase` 对 `mysql`、`information_schema`、`performance_schema`、`sys` 返回 true,写入类 API 与前端 `readOnly` 分支统一依赖该逻辑。
78
+
79
+ ## 连接存储
80
+
81
+ 连接配置保存在项目工作目录 `.f2e_cache/mysql-connection.json`(见 `server/dao/connection.ts` 的 `DBFile` 配置)。
21
82
 
22
83
  ## 构建服务端
23
84
 
@@ -25,8 +86,4 @@ npm run dev # 前端 + API 联调
25
86
  npm run build:server
26
87
  ```
27
88
 
28
- `mysql2` 在 esbuild 中标记为 `external`,运行时使用 Node 解析。
29
-
30
- ## 主键与行编辑
31
-
32
- 列表接口为每行附加 `__pk`(主键列快照)。更新 / 删除使用主键定位;无主键表不开放行级写操作。
89
+ `mysql2` 在 esbuild 中标记为 `external`,运行时使用 Node 解析;`f2e-server3` 同样 external,由运行环境提供。
package/README.md CHANGED
@@ -12,25 +12,74 @@ npm run dev
12
12
 
13
13
  默认开发端口为 **7018**(与 mongo-dashboard 的 7017 错开)。浏览器访问控制台输出的地址即可。
14
14
 
15
+ 生产环境可先构建再启动:
16
+
17
+ ```bash
18
+ npm run build
19
+ npm start
20
+ ```
21
+
22
+ 也可全局安装后使用命令行(默认端口同样可通过 `-p` 指定):
23
+
24
+ ```bash
25
+ npm install -g mysql-dashboard
26
+ mysql-dashboard -m prod -p 7018
27
+ ```
28
+
29
+ ## 界面与导航
30
+
31
+ - **连接**:首页 `/` 管理已保存连接(增删改、测试连通性)。连接字段包含名称、主机、端口、用户名、密码、可选默认库,以及 JDBC 风格的 `mysql://...` 连接串(`connectionString`)。
32
+ - **数据库**:`/connection/:connectionId` 列出库及估算占用,支持创建 / 重命名(将旧库下全部表 `RENAME TABLE` 迁至新库后删除旧库)/ 删除。系统库仅可查看,不可删除或重命名。
33
+ - **数据表**:`/connection/:connectionId/database/:databaseName` 列出当前库下的基表(`information_schema`),支持新建表、重命名、删除;可编辑表结构(新增列、修改列属性或重命名列、删除列)。新建表时若不指定列,则创建带 `INT AUTO_INCREMENT PRIMARY KEY` 的空表;也可按向导定义多列后创建。
34
+ - **表数据与查询**:`/connection/:connectionId/database/:databaseName/collection/:collectionName` 分页浏览行数据、列显示设置、按 JSON 编辑行(需主键)、删除行(需主键),并打开「自定义查询」面板。
35
+
36
+ 顶部可切换**浅色 / 深色**主题;**帮助**菜单中可打开本页(帮助手册)与开发手册。
37
+
15
38
  ## 功能概览
16
39
 
17
- - **连接管理**:主机、端口、用户名、密码、可选默认库与 JDBC 风格 URI(`mysql://...`)
18
- - **数据库**:列表、估算占用、创建 / 重命名(迁移全部表)/ 删除;系统库只读保护
19
- - **数据表**:列表(基于 `information_schema`)、新建(自增主键空表)、重命名、删除
20
- - **数据行**:分页浏览、列显示设置、按主键更新 / 删除(无主键表仅支持浏览)
21
- - **查询**:等值条件 JSON 查询、计数、`SELECT` 自定义查询、JSON 插入一行
40
+ ### 连接管理
41
+
42
+ - 列表、创建、更新、删除、连接测试。
43
+ - 配置持久保存在项目目录 `.f2e_cache/mysql-connection.json`(由 f2e-server3 的 `DBFile` 维护)。
44
+
45
+ ### 数据库
46
+
47
+ - 列表与占用估算(`information_schema`)。
48
+ - 创建、删除;重命名通过建新库并迁移全部表实现。
49
+ - **系统库** `mysql`、`information_schema`、`performance_schema`、`sys`:禁止删除、重命名,且其下不允许建表、改表、行级写入等变更操作;只读浏览不受影响。
50
+
51
+ ### 数据表
52
+
53
+ - 列表展示近似行数、数据量与索引量等(`TABLE_ROWS` 等为估计值)。
54
+ - 新建(默认单列自增主键表或自定义列)、重命名、删除。
55
+ - **表结构**:查看列类型、可空、键、默认值、`EXTRA`、注释等;在允许的库中可 **ALTER**:`ADD` / `MODIFY`(含列重命名)/ `DROP COLUMN`。
56
+
57
+ ### 数据行
58
+
59
+ - 分页列表;可配置列显示。
60
+ - 每行附带内部字段 `__pk`(主键列快照),用于定位更新与删除;**无主键**的表仅支持浏览,不提供行更新 / 删除。
61
+ - 使用「自定义查询」返回的结果若来自非标准列表路径,可能缺少完整 `__pk`,界面会提示此时行编辑 / 删除可能不可用。
62
+
63
+ ### 查询(自定义查询面板)
64
+
65
+ | 操作 | 说明 |
66
+ |------|------|
67
+ | **find** | 等值条件 JSON(键为列名,合法标识符),`WHERE` 为 `AND` 连接;空对象 `{}` 表示无额外条件;最多返回 **100** 行。 |
68
+ | **findOne** | 同上,最多 1 行。 |
69
+ | **count** | 同上,返回匹配行数。 |
70
+ | **aggregate**(界面文案为自定义 SELECT) | 仅限以 `SELECT` 开头的语句;禁止 `INTO`、`OUTFILE`、`DUMPFILE`、`FOR UPDATE` 等关键字。 |
71
+ | **insertOne** | 提供 JSON 对象,键为列名、值为单元格值,执行 `INSERT`;系统库与非只读模式下策略与表详情页一致。 |
72
+
73
+ 等值条件中的键名仅允许字母、数字、下划线(与后端拼装一致)。
22
74
 
23
75
  ## 环境要求
24
76
 
25
77
  - Node.js >= 18
26
78
  - 可访问的 MySQL 5.7+ / 8.x 实例
27
79
 
28
- ## 生产构建
80
+ ## 应用内帮助
29
81
 
30
- ```bash
31
- npm run build
32
- npm start
33
- ```
82
+ 构建后的站点中,通过顶部 **帮助 → 帮助手册 / 开发手册** 可阅读本仓库的 `README.md` 与 `DEVELOPMENT.md`(路由 `/help/readme`、`/help/development`)。
34
83
 
35
84
  ## 安全提示
36
85
 
package/lib/index.js CHANGED
@@ -179,16 +179,17 @@ var import_f2e_server33 = require("f2e-server3");
179
179
 
180
180
  // server/utils/database.ts
181
181
  var RESERVED = ["mysql", "information_schema", "performance_schema", "sys"];
182
- function isReadOnlyDatabase(databaseName) {
183
- return RESERVED.includes(databaseName.toLowerCase());
184
- }
185
-
186
- // server/api/database.ts
187
182
  function assertIdent(name, label) {
183
+ return;
188
184
  if (!/^[a-zA-Z0-9_]+$/.test(name)) {
189
185
  throw new Error(`${label}\u540D\u79F0\u5305\u542B\u975E\u6CD5\u5B57\u7B26`);
190
186
  }
191
187
  }
188
+ function isReadOnlyDatabase(databaseName) {
189
+ return RESERVED.includes(databaseName.toLowerCase());
190
+ }
191
+
192
+ // server/api/database.ts
192
193
  (0, import_f2e_server33.addRoute)("/api/database/list", async (body) => {
193
194
  const connections = appStorage.data.connections || [];
194
195
  const connection = connections.find((c) => c._id === body.connectionId);
@@ -339,11 +340,155 @@ function assertIdent(name, label) {
339
340
 
340
341
  // server/api/collection.ts
341
342
  var import_f2e_server34 = require("f2e-server3");
342
- function assertIdent2(name, label) {
343
- return;
343
+ var import_mysql22 = require("mysql2");
344
+ var COLUMN_TYPE_PATTERN = /^[a-zA-Z][a-zA-Z0-9_() ,-]*$/;
345
+ function assertColumnName(name) {
344
346
  if (!/^[a-zA-Z0-9_]+$/.test(name)) {
345
- throw new Error(`${label}\u540D\u79F0\u5305\u542B\u975E\u6CD5\u5B57\u7B26`);
347
+ throw new Error(`\u5B57\u6BB5\u540D\u5305\u542B\u975E\u6CD5\u5B57\u7B26: ${name}`);
348
+ }
349
+ }
350
+ function assertColumnType(columnType) {
351
+ const t = columnType.trim();
352
+ if (!t || t.length > 200 || !COLUMN_TYPE_PATTERN.test(t)) {
353
+ throw new Error(`\u5B57\u6BB5\u7C7B\u578B\u4E0D\u5408\u6CD5: ${columnType}`);
354
+ }
355
+ }
356
+ function buildCommentSql(comment) {
357
+ const c = comment?.trim();
358
+ if (!c) {
359
+ return "";
360
+ }
361
+ return ` COMMENT ${(0, import_mysql22.escape)(c)}`;
362
+ }
363
+ function buildDefaultSql(draft) {
364
+ if (draft.autoIncrement) {
365
+ return "";
366
+ }
367
+ const raw = draft.defaultValue;
368
+ if (raw === void 0 || raw === null) {
369
+ return "";
370
+ }
371
+ const v = String(raw).trim();
372
+ if (!v) {
373
+ return "";
374
+ }
375
+ const upper = v.toUpperCase();
376
+ if (upper === "NULL") {
377
+ if (!draft.nullable) {
378
+ throw new Error(`\u5B57\u6BB5 ${draft.name}: \u975E\u7A7A\u5217\u4E0D\u80FD\u4F7F\u7528 DEFAULT NULL`);
379
+ }
380
+ return " DEFAULT NULL";
381
+ }
382
+ if (upper === "CURRENT_TIMESTAMP" || upper === "CURRENT_TIMESTAMP()" || upper === "CURRENT_DATE" || upper === "CURRENT_TIME" || upper === "LOCALTIMESTAMP" || upper === "LOCALTIMESTAMP()" || upper === "UUID()") {
383
+ return ` DEFAULT ${v}`;
384
+ }
385
+ if (/^-?\d+(\.\d+)?$/.test(v)) {
386
+ return ` DEFAULT ${v}`;
387
+ }
388
+ return ` DEFAULT ${(0, import_mysql22.escape)(v)}`;
389
+ }
390
+ function buildColumnAttributesSql(draft) {
391
+ assertColumnType(draft.columnType);
392
+ const nullableEff = draft.primaryKey ? false : draft.nullable;
393
+ let sql = draft.columnType.trim();
394
+ if (draft.autoIncrement) {
395
+ sql += " NOT NULL";
396
+ } else {
397
+ sql += nullableEff ? " NULL" : " NOT NULL";
398
+ }
399
+ sql += buildDefaultSql({ ...draft, nullable: nullableEff });
400
+ if (draft.autoIncrement) {
401
+ sql += " AUTO_INCREMENT";
402
+ }
403
+ sql += buildCommentSql(draft.comment);
404
+ return sql;
405
+ }
406
+ function buildColumnBodySql(draft, forName) {
407
+ assertColumnName(forName);
408
+ return `${escapeIdentifier(forName)} ${buildColumnAttributesSql(draft)}`;
409
+ }
410
+ function validateDraftColumns(columns) {
411
+ const names = /* @__PURE__ */ new Set();
412
+ let ai = 0;
413
+ for (const c of columns) {
414
+ if (!c.name?.trim()) {
415
+ throw new Error("\u5B57\u6BB5\u540D\u4E0D\u80FD\u4E3A\u7A7A");
416
+ }
417
+ assertColumnName(c.name.trim());
418
+ const n = c.name.trim();
419
+ if (names.has(n)) {
420
+ throw new Error(`\u91CD\u590D\u5B57\u6BB5\u540D: ${n}`);
421
+ }
422
+ names.add(n);
423
+ assertColumnType(c.columnType);
424
+ if (c.autoIncrement) {
425
+ ai += 1;
426
+ if (!c.primaryKey) {
427
+ throw new Error(`\u81EA\u589E\u5217\u5FC5\u987B\u662F\u4E3B\u952E: ${n}`);
428
+ }
429
+ }
430
+ }
431
+ if (ai > 1) {
432
+ throw new Error("\u4E00\u5F20\u8868\u6700\u591A\u53EA\u80FD\u6709\u4E00\u4E2A\u81EA\u589E\u5217");
433
+ }
434
+ }
435
+ function buildCreateTableStatement(tableIdent, columns) {
436
+ if (columns.length === 0) {
437
+ throw new Error("\u81F3\u5C11\u5B9A\u4E49\u4E00\u4E2A\u5B57\u6BB5");
438
+ }
439
+ validateDraftColumns(columns);
440
+ const trimmed = columns.map((c) => ({
441
+ ...c,
442
+ name: c.name.trim()
443
+ }));
444
+ const pkCols = trimmed.filter((c) => c.primaryKey).map((c) => c.name);
445
+ const lines = trimmed.map((c) => buildColumnBodySql({ ...c, name: c.name }, c.name));
446
+ if (pkCols.length > 0) {
447
+ lines.push(`PRIMARY KEY (${pkCols.map((cn) => escapeIdentifier(cn)).join(", ")})`);
346
448
  }
449
+ return `CREATE TABLE ${tableIdent} (
450
+ ${lines.join(",\n ")}
451
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`;
452
+ }
453
+ function buildAddColumnClause(col) {
454
+ assertColumnName(col.name.trim());
455
+ assertColumnType(col.columnType);
456
+ const draft = {
457
+ name: col.name.trim(),
458
+ columnType: col.columnType,
459
+ nullable: col.nullable,
460
+ defaultValue: col.defaultValue,
461
+ primaryKey: false,
462
+ autoIncrement: false,
463
+ comment: col.comment
464
+ };
465
+ let clause = buildColumnBodySql(draft, draft.name);
466
+ if (col.afterColumn?.trim()) {
467
+ assertIdent(col.afterColumn.trim(), "\u5B57\u6BB5");
468
+ clause += ` AFTER ${escapeIdentifier(col.afterColumn.trim())}`;
469
+ }
470
+ return clause;
471
+ }
472
+ function buildModifyDraft(m) {
473
+ const targetName = m.newName?.trim() && m.newName.trim() !== m.columnName ? m.newName.trim() : m.columnName;
474
+ assertColumnName(m.columnName);
475
+ if (targetName !== m.columnName) {
476
+ assertColumnName(targetName);
477
+ }
478
+ assertColumnType(m.columnType);
479
+ const draft = {
480
+ name: targetName,
481
+ columnType: m.columnType,
482
+ nullable: m.nullable,
483
+ defaultValue: m.defaultValue,
484
+ primaryKey: false,
485
+ autoIncrement: Boolean(m.autoIncrement),
486
+ comment: m.comment
487
+ };
488
+ if (draft.autoIncrement && m.nullable) {
489
+ throw new Error("\u81EA\u589E\u5217\u5FC5\u987B NOT NULL");
490
+ }
491
+ return draft;
347
492
  }
348
493
  async function getPrimaryKeyColumns(conn, databaseName, tableName) {
349
494
  const [rows] = await conn.query(
@@ -386,7 +531,7 @@ function stripMetaFields(obj) {
386
531
  };
387
532
  }
388
533
  try {
389
- assertIdent2(body.databaseName, "\u6570\u636E\u5E93");
534
+ assertIdent(body.databaseName, "\u6570\u636E\u5E93");
390
535
  const collections = await withMysqlConnection(connection, async (conn) => {
391
536
  const [rows] = await conn.query(
392
537
  `SELECT TABLE_NAME AS name,
@@ -437,8 +582,8 @@ function stripMetaFields(obj) {
437
582
  };
438
583
  }
439
584
  try {
440
- assertIdent2(body.databaseName, "\u6570\u636E\u5E93");
441
- assertIdent2(body.collectionName, "\u6570\u636E\u8868");
585
+ assertIdent(body.databaseName, "\u6570\u636E\u5E93");
586
+ assertIdent(body.collectionName, "\u6570\u636E\u8868");
442
587
  const columns = await withMysqlConnection(connection, async (conn) => {
443
588
  const [colRows] = await conn.query(
444
589
  `SELECT COLUMN_NAME, ORDINAL_POSITION, COLUMN_TYPE, DATA_TYPE,
@@ -495,8 +640,8 @@ function stripMetaFields(obj) {
495
640
  const pageSize = body.pageSize || 20;
496
641
  const skip = (page - 1) * pageSize;
497
642
  try {
498
- assertIdent2(body.databaseName, "\u6570\u636E\u5E93");
499
- assertIdent2(body.collectionName, "\u6570\u636E\u8868");
643
+ assertIdent(body.databaseName, "\u6570\u636E\u5E93");
644
+ assertIdent(body.collectionName, "\u6570\u636E\u8868");
500
645
  const table = escapeIdentifier(body.collectionName);
501
646
  const result = await withMysqlConnection(connection, async (conn) => {
502
647
  await conn.query(`USE ${escapeIdentifier(body.databaseName)}`);
@@ -547,8 +692,8 @@ function stripMetaFields(obj) {
547
692
  };
548
693
  }
549
694
  try {
550
- assertIdent2(body.databaseName, "\u6570\u636E\u5E93");
551
- assertIdent2(body.collectionName, "\u6570\u636E\u8868");
695
+ assertIdent(body.databaseName, "\u6570\u636E\u5E93");
696
+ assertIdent(body.collectionName, "\u6570\u636E\u8868");
552
697
  const doc = stripMetaFields(body.document);
553
698
  const keys = Object.keys(doc);
554
699
  if (keys.length === 0) {
@@ -592,8 +737,8 @@ function stripMetaFields(obj) {
592
737
  };
593
738
  }
594
739
  try {
595
- assertIdent2(body.databaseName, "\u6570\u636E\u5E93");
596
- assertIdent2(body.collectionName, "\u6570\u636E\u8868");
740
+ assertIdent(body.databaseName, "\u6570\u636E\u5E93");
741
+ assertIdent(body.collectionName, "\u6570\u636E\u8868");
597
742
  const table = escapeIdentifier(body.collectionName);
598
743
  await withMysqlConnection(connection, async (conn) => {
599
744
  await conn.query(`USE ${escapeIdentifier(body.databaseName)}`);
@@ -659,8 +804,8 @@ function stripMetaFields(obj) {
659
804
  };
660
805
  }
661
806
  try {
662
- assertIdent2(body.databaseName, "\u6570\u636E\u5E93");
663
- assertIdent2(body.collectionName, "\u6570\u636E\u8868");
807
+ assertIdent(body.databaseName, "\u6570\u636E\u5E93");
808
+ assertIdent(body.collectionName, "\u6570\u636E\u8868");
664
809
  const table = escapeIdentifier(body.collectionName);
665
810
  await withMysqlConnection(connection, async (conn) => {
666
811
  await conn.query(`USE ${escapeIdentifier(body.databaseName)}`);
@@ -691,44 +836,128 @@ function stripMetaFields(obj) {
691
836
  }
692
837
  }
693
838
  );
694
- (0, import_f2e_server34.addRoute)("/api/collection/create", async (body) => {
695
- const connections = appStorage.data.connections || [];
696
- const connection = connections.find((c) => c._id === body.connectionId);
697
- if (!connection) {
698
- return {
699
- success: false,
700
- message: "\u8FDE\u63A5\u4E0D\u5B58\u5728"
701
- };
702
- }
703
- if (isReadOnlyDatabase(body.databaseName)) {
704
- return {
705
- success: false,
706
- message: "\u7CFB\u7EDF\u5E93\u4E0D\u5141\u8BB8\u5199\u5165"
707
- };
708
- }
709
- try {
710
- assertIdent2(body.databaseName, "\u6570\u636E\u5E93");
711
- assertIdent2(body.collectionName, "\u6570\u636E\u8868");
712
- const table = escapeIdentifier(body.collectionName);
713
- await withMysqlConnection(connection, async (conn) => {
714
- await conn.query(`USE ${escapeIdentifier(body.databaseName)}`);
715
- await conn.query(
716
- `CREATE TABLE ${table} (
839
+ (0, import_f2e_server34.addRoute)(
840
+ "/api/collection/create",
841
+ async (body) => {
842
+ const connections = appStorage.data.connections || [];
843
+ const connection = connections.find((c) => c._id === body.connectionId);
844
+ if (!connection) {
845
+ return {
846
+ success: false,
847
+ message: "\u8FDE\u63A5\u4E0D\u5B58\u5728"
848
+ };
849
+ }
850
+ if (isReadOnlyDatabase(body.databaseName)) {
851
+ return {
852
+ success: false,
853
+ message: "\u7CFB\u7EDF\u5E93\u4E0D\u5141\u8BB8\u5199\u5165"
854
+ };
855
+ }
856
+ try {
857
+ assertIdent(body.databaseName, "\u6570\u636E\u5E93");
858
+ assertIdent(body.collectionName, "\u6570\u636E\u8868");
859
+ const table = escapeIdentifier(body.collectionName);
860
+ await withMysqlConnection(connection, async (conn) => {
861
+ await conn.query(`USE ${escapeIdentifier(body.databaseName)}`);
862
+ const cols = body.columns;
863
+ if (cols && cols.length > 0) {
864
+ const sql = buildCreateTableStatement(table, cols);
865
+ await conn.query(sql);
866
+ } else {
867
+ await conn.query(
868
+ `CREATE TABLE ${table} (
717
869
  id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
718
870
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`
719
- );
720
- });
721
- return {
722
- success: true,
723
- message: "\u521B\u5EFA\u6570\u636E\u8868\u6210\u529F"
724
- };
725
- } catch (error) {
726
- return {
727
- success: false,
728
- message: error instanceof Error ? error.message : "\u521B\u5EFA\u6570\u636E\u8868\u5931\u8D25"
729
- };
871
+ );
872
+ }
873
+ });
874
+ return {
875
+ success: true,
876
+ message: "\u521B\u5EFA\u6570\u636E\u8868\u6210\u529F"
877
+ };
878
+ } catch (error) {
879
+ return {
880
+ success: false,
881
+ message: error instanceof Error ? error.message : "\u521B\u5EFA\u6570\u636E\u8868\u5931\u8D25"
882
+ };
883
+ }
730
884
  }
731
- });
885
+ );
886
+ (0, import_f2e_server34.addRoute)(
887
+ "/api/collection/alter-schema",
888
+ async (body) => {
889
+ const connections = appStorage.data.connections || [];
890
+ const connection = connections.find((c) => c._id === body.connectionId);
891
+ if (!connection) {
892
+ return {
893
+ success: false,
894
+ message: "\u8FDE\u63A5\u4E0D\u5B58\u5728"
895
+ };
896
+ }
897
+ if (isReadOnlyDatabase(body.databaseName)) {
898
+ return {
899
+ success: false,
900
+ message: "\u7CFB\u7EDF\u5E93\u4E0D\u5141\u8BB8\u5199\u5165"
901
+ };
902
+ }
903
+ const adds = body.addColumns || [];
904
+ const mods = body.modifyColumns || [];
905
+ const drops = body.dropColumns || [];
906
+ if (adds.length === 0 && mods.length === 0 && drops.length === 0) {
907
+ return {
908
+ success: false,
909
+ message: "\u6CA1\u6709\u8981\u6267\u884C\u7684\u8868\u7ED3\u6784\u53D8\u66F4"
910
+ };
911
+ }
912
+ try {
913
+ assertIdent(body.databaseName, "\u6570\u636E\u5E93");
914
+ assertIdent(body.collectionName, "\u6570\u636E\u8868");
915
+ for (const d of drops) {
916
+ assertColumnName(d);
917
+ }
918
+ const table = escapeIdentifier(body.collectionName);
919
+ await withMysqlConnection(connection, async (conn) => {
920
+ await conn.query(`USE ${escapeIdentifier(body.databaseName)}`);
921
+ await conn.beginTransaction();
922
+ try {
923
+ for (const m of mods) {
924
+ const draft = buildModifyDraft(m);
925
+ const newN = m.newName?.trim() && m.newName.trim() !== m.columnName ? m.newName.trim() : m.columnName;
926
+ const attr = buildColumnAttributesSql(draft);
927
+ if (newN !== m.columnName) {
928
+ await conn.query(
929
+ `ALTER TABLE ${table} CHANGE COLUMN ${escapeIdentifier(m.columnName)} ${escapeIdentifier(newN)} ${attr}`
930
+ );
931
+ } else {
932
+ await conn.query(
933
+ `ALTER TABLE ${table} MODIFY COLUMN ${escapeIdentifier(m.columnName)} ${attr}`
934
+ );
935
+ }
936
+ }
937
+ for (const a of adds) {
938
+ await conn.query(`ALTER TABLE ${table} ADD COLUMN ${buildAddColumnClause(a)}`);
939
+ }
940
+ for (const d of drops) {
941
+ await conn.query(`ALTER TABLE ${table} DROP COLUMN ${escapeIdentifier(d)}`);
942
+ }
943
+ await conn.commit();
944
+ } catch (e) {
945
+ await conn.rollback();
946
+ throw e;
947
+ }
948
+ });
949
+ return {
950
+ success: true,
951
+ message: "\u4FEE\u6539\u8868\u7ED3\u6784\u6210\u529F"
952
+ };
953
+ } catch (error) {
954
+ return {
955
+ success: false,
956
+ message: error instanceof Error ? error.message : "\u4FEE\u6539\u8868\u7ED3\u6784\u5931\u8D25"
957
+ };
958
+ }
959
+ }
960
+ );
732
961
  (0, import_f2e_server34.addRoute)("/api/collection/delete-collection", async (body) => {
733
962
  const connections = appStorage.data.connections || [];
734
963
  const connection = connections.find((c) => c._id === body.connectionId);
@@ -745,8 +974,8 @@ function stripMetaFields(obj) {
745
974
  };
746
975
  }
747
976
  try {
748
- assertIdent2(body.databaseName, "\u6570\u636E\u5E93");
749
- assertIdent2(body.collectionName, "\u6570\u636E\u8868");
977
+ assertIdent(body.databaseName, "\u6570\u636E\u5E93");
978
+ assertIdent(body.collectionName, "\u6570\u636E\u8868");
750
979
  const table = escapeIdentifier(body.collectionName);
751
980
  await withMysqlConnection(connection, async (conn) => {
752
981
  await conn.query(`USE ${escapeIdentifier(body.databaseName)}`);
@@ -781,9 +1010,9 @@ function stripMetaFields(obj) {
781
1010
  };
782
1011
  }
783
1012
  try {
784
- assertIdent2(body.databaseName, "\u6570\u636E\u5E93");
785
- assertIdent2(body.oldCollectionName, "\u6570\u636E\u8868");
786
- assertIdent2(body.newCollectionName, "\u6570\u636E\u8868");
1013
+ assertIdent(body.databaseName, "\u6570\u636E\u5E93");
1014
+ assertIdent(body.oldCollectionName, "\u6570\u636E\u8868");
1015
+ assertIdent(body.newCollectionName, "\u6570\u636E\u8868");
787
1016
  const oldT = escapeIdentifier(body.oldCollectionName);
788
1017
  const newT = escapeIdentifier(body.newCollectionName);
789
1018
  await withMysqlConnection(connection, async (conn) => {
@@ -805,11 +1034,6 @@ function stripMetaFields(obj) {
805
1034
 
806
1035
  // server/api/query.ts
807
1036
  var import_f2e_server35 = require("f2e-server3");
808
- function assertIdent3(name, label) {
809
- if (!/^[a-zA-Z0-9_]+$/.test(name)) {
810
- throw new Error(`${label}\u540D\u79F0\u5305\u542B\u975E\u6CD5\u5B57\u7B26`);
811
- }
812
- }
813
1037
  function parseJsonObject(query) {
814
1038
  const trimmed = query.trim();
815
1039
  if (!trimmed) {
@@ -857,8 +1081,8 @@ function assertSafeSelect(sql) {
857
1081
  message: "\u7CFB\u7EDF\u5E93\u4E0D\u5141\u8BB8\u5199\u5165"
858
1082
  };
859
1083
  }
860
- assertIdent3(body.databaseName, "\u6570\u636E\u5E93");
861
- assertIdent3(body.collectionName, "\u6570\u636E\u8868");
1084
+ assertIdent(body.databaseName, "\u6570\u636E\u5E93");
1085
+ assertIdent(body.collectionName, "\u6570\u636E\u8868");
862
1086
  const table = escapeIdentifier(body.collectionName);
863
1087
  try {
864
1088
  const result = await withMysqlConnection(connection, async (conn) => {
package/output/index.html CHANGED
@@ -4,10 +4,10 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>mysql-dashboard</title>
7
- <link rel="stylesheet" href="/static/index.css?197e505f">
7
+ <link rel="stylesheet" href="/static/index.css?775f1819">
8
8
  </head>
9
9
  <body>
10
10
  <div id="root"></div>
11
- <script src="/static/index.js?6e361ab2"></script>
11
+ <script src="/static/index.js?c8dbb6da"></script>
12
12
  </body>
13
13
  </html>