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 +65 -8
- package/README.md +59 -10
- package/lib/index.js +290 -66
- package/output/index.html +2 -2
- package/output/static/index.css +21 -0
- package/output/static/index.css.map +1 -1
- package/output/static/index.js +75 -139
- package/output/static/index.js.map +4 -4
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
441
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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
|
-
|
|
551
|
-
|
|
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
|
-
|
|
596
|
-
|
|
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
|
-
|
|
663
|
-
|
|
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)(
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
await conn
|
|
716
|
-
`
|
|
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
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
749
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
861
|
-
|
|
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?
|
|
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?
|
|
11
|
+
<script src="/static/index.js?c8dbb6da"></script>
|
|
12
12
|
</body>
|
|
13
13
|
</html>
|