@lark-apaas/miaoda-cli 0.1.0-alpha.c783fb5 → 0.1.0-alpha.ca2912e
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/dist/api/db/api.js +162 -0
- package/dist/api/db/client.js +126 -0
- package/dist/api/db/index.js +16 -0
- package/dist/api/db/parsers.js +157 -0
- package/dist/api/db/types.js +10 -0
- package/dist/api/file/api.js +420 -0
- package/dist/api/file/client.js +163 -0
- package/dist/api/file/detect.js +56 -0
- package/dist/api/file/index.js +17 -0
- package/dist/api/file/parsers.js +72 -0
- package/dist/api/file/types.js +3 -0
- package/dist/api/index.js +5 -1
- package/dist/api/plugin/api.js +3 -3
- package/dist/cli/commands/db/index.js +75 -0
- package/dist/cli/commands/file/index.js +67 -0
- package/dist/cli/commands/index.js +4 -0
- package/dist/cli/commands/shared.js +8 -3
- package/dist/cli/handlers/db/data.js +167 -0
- package/dist/cli/handlers/db/index.js +11 -0
- package/dist/cli/handlers/db/schema.js +163 -0
- package/dist/cli/handlers/db/sql.js +179 -0
- package/dist/cli/handlers/file/cp.js +220 -0
- package/dist/cli/handlers/file/index.js +13 -0
- package/dist/cli/handlers/file/ls.js +109 -0
- package/dist/cli/handlers/file/rm.js +243 -0
- package/dist/cli/handlers/file/sign.js +96 -0
- package/dist/cli/handlers/file/stat.js +97 -0
- package/dist/cli/handlers/index.js +2 -0
- package/dist/utils/http.js +31 -10
- package/dist/utils/index.js +3 -1
- package/dist/utils/output.js +15 -5
- package/dist/utils/render.js +187 -0
- package/package.json +2 -2
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.execSql = execSql;
|
|
4
|
+
exports.getSchema = getSchema;
|
|
5
|
+
exports.importData = importData;
|
|
6
|
+
exports.exportData = exportData;
|
|
7
|
+
const http_1 = require("../../utils/http");
|
|
8
|
+
const error_1 = require("../../utils/error");
|
|
9
|
+
const client_1 = require("./client");
|
|
10
|
+
// CLI 不再为 dbBranch 设默认值:
|
|
11
|
+
// 用户没传 --env 就完全不携带 dbBranch query 参数,由后端 admin-inner 中间件
|
|
12
|
+
// 按 workspace 多环境状态决定(多环境 → dev / 单环境 → main)。
|
|
13
|
+
// 这样可以避免单环境应用被强行打到 main 之外、或多环境应用被默认打到 main 上线库。
|
|
14
|
+
// ── db sql → InnerAdminExecuteSQL ──
|
|
15
|
+
/**
|
|
16
|
+
* 执行 SQL(admin-inner)。
|
|
17
|
+
* 后端:POST /v1/dataloom/app/{appId}/db/sql?dbBranch=main
|
|
18
|
+
*
|
|
19
|
+
* 返回所有 results[](多条语句时每条一项);CLI 侧按 PRD 仅取最后一条展示,
|
|
20
|
+
* 但 API 层保留完整列表以便测试和高级用法。
|
|
21
|
+
*/
|
|
22
|
+
async function execSql(opts) {
|
|
23
|
+
const client = (0, http_1.getHttpClient)();
|
|
24
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/sql", {
|
|
25
|
+
dbBranch: opts.dbBranch,
|
|
26
|
+
});
|
|
27
|
+
const response = await client.post(url, { sql: opts.sql });
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
// 4xx / 5xx:尝试解析 body 取 status_code 映射业务错误
|
|
30
|
+
let body = null;
|
|
31
|
+
try {
|
|
32
|
+
body = (await response.json());
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// ignore
|
|
36
|
+
}
|
|
37
|
+
if (body)
|
|
38
|
+
(0, client_1.extractData)(body);
|
|
39
|
+
throw new error_1.HttpError(response.status, url, `Failed to execute SQL: ${String(response.status)} ${response.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
const body = (await response.json());
|
|
42
|
+
const data = (0, client_1.extractData)(body);
|
|
43
|
+
return data.results ?? [];
|
|
44
|
+
}
|
|
45
|
+
// ── db schema → InnerGetSchema ──
|
|
46
|
+
/**
|
|
47
|
+
* 查询 schema(admin-inner)。
|
|
48
|
+
* 后端:GET /v1/dataloom/app/{appId}/db/schema?format=schema|ddl&tableNames=...&includeStats=...&dbBranch=main
|
|
49
|
+
*
|
|
50
|
+
* 返回 body.data(含 `schema` 或 `ddl`)供 handler 使用。
|
|
51
|
+
*/
|
|
52
|
+
async function getSchema(opts) {
|
|
53
|
+
const client = (0, http_1.getHttpClient)();
|
|
54
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/schema", {
|
|
55
|
+
format: opts.format ?? "schema",
|
|
56
|
+
tableNames: opts.tableNames,
|
|
57
|
+
includeStats: opts.includeStats ? "true" : undefined,
|
|
58
|
+
dbBranch: opts.dbBranch,
|
|
59
|
+
});
|
|
60
|
+
const response = await client.get(url);
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
let body = null;
|
|
63
|
+
try {
|
|
64
|
+
body = (await response.json());
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// ignore
|
|
68
|
+
}
|
|
69
|
+
if (body)
|
|
70
|
+
(0, client_1.extractData)(body);
|
|
71
|
+
throw new error_1.HttpError(response.status, url, `Failed to get schema: ${String(response.status)} ${response.statusText}`);
|
|
72
|
+
}
|
|
73
|
+
const body = (await response.json());
|
|
74
|
+
return (0, client_1.extractData)(body);
|
|
75
|
+
}
|
|
76
|
+
// ── db data import → InnerImportData ──
|
|
77
|
+
/**
|
|
78
|
+
* 导入文件。
|
|
79
|
+
* 后端:POST /v1/dataloom/app/{appId}/data/import?tableName=...&format=csv|json&dbBranch=main
|
|
80
|
+
*
|
|
81
|
+
* Body 为原始文件字节(不走 JSON envelope)。
|
|
82
|
+
*/
|
|
83
|
+
async function importData(opts) {
|
|
84
|
+
const client = (0, http_1.getHttpClient)();
|
|
85
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import", {
|
|
86
|
+
tableName: opts.tableName,
|
|
87
|
+
format: opts.format,
|
|
88
|
+
dbBranch: opts.dbBranch,
|
|
89
|
+
});
|
|
90
|
+
const contentType = opts.format === "csv" ? "text/csv" : "application/json";
|
|
91
|
+
const ab = opts.body.buffer.slice(opts.body.byteOffset, opts.body.byteOffset + opts.body.byteLength);
|
|
92
|
+
const response = await client.request({
|
|
93
|
+
method: "POST",
|
|
94
|
+
url,
|
|
95
|
+
headers: { "Content-Type": contentType },
|
|
96
|
+
body: ab,
|
|
97
|
+
});
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
let body = null;
|
|
100
|
+
try {
|
|
101
|
+
body = (await response.json());
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// ignore
|
|
105
|
+
}
|
|
106
|
+
if (body)
|
|
107
|
+
(0, client_1.extractData)(body);
|
|
108
|
+
throw new error_1.HttpError(response.status, url, `Failed to import data: ${String(response.status)} ${response.statusText}`);
|
|
109
|
+
}
|
|
110
|
+
// 后端 InnerImportData 响应里 data 直接返 {tableName, rows, durationMs}
|
|
111
|
+
const body = (await response.json());
|
|
112
|
+
const data = (0, client_1.extractData)(body);
|
|
113
|
+
return {
|
|
114
|
+
tableName: data.tableName ?? opts.tableName,
|
|
115
|
+
rows: data.rows ?? 0,
|
|
116
|
+
durationMs: data.durationMs ?? 0,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// ── db data export → InnerExportData ──
|
|
120
|
+
/**
|
|
121
|
+
* 导出数据。
|
|
122
|
+
* 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&dbBranch=main
|
|
123
|
+
*
|
|
124
|
+
* 响应 body 为原始 CSV/JSON 字节(不走 envelope)。错误仍通过 HTTP 4xx/5xx + BaseResp 传达。
|
|
125
|
+
*/
|
|
126
|
+
async function exportData(opts) {
|
|
127
|
+
const client = (0, http_1.getHttpClient)();
|
|
128
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/export", {
|
|
129
|
+
tableName: opts.tableName,
|
|
130
|
+
format: opts.format,
|
|
131
|
+
dbBranch: opts.dbBranch,
|
|
132
|
+
});
|
|
133
|
+
const reqBody = { limit: opts.limit ?? 5000 };
|
|
134
|
+
const response = await client.post(url, reqBody);
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
// 错误路径:body 是 JSON envelope
|
|
137
|
+
let body = null;
|
|
138
|
+
try {
|
|
139
|
+
body = (await response.json());
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// ignore
|
|
143
|
+
}
|
|
144
|
+
if (body)
|
|
145
|
+
(0, client_1.extractData)(body);
|
|
146
|
+
throw new error_1.HttpError(response.status, url, `Failed to export data: ${String(response.status)} ${response.statusText}`);
|
|
147
|
+
}
|
|
148
|
+
// 成功路径:响应 body 是原始 CSV/JSON 字节
|
|
149
|
+
const contentType = response.headers.get("Content-Type") ??
|
|
150
|
+
(opts.format === "csv" ? "text/csv" : "application/json");
|
|
151
|
+
const ab = await response.arrayBuffer();
|
|
152
|
+
const buf = Buffer.from(new Uint8Array(ab));
|
|
153
|
+
if (buf.length === 0) {
|
|
154
|
+
throw new error_1.AppError("INTERNAL_DB_ERROR", "Empty export response body");
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
tableName: opts.tableName,
|
|
158
|
+
format: opts.format,
|
|
159
|
+
contentType,
|
|
160
|
+
body: buf,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SQLSTATE_MAP = void 0;
|
|
4
|
+
exports.ensureInnerSuccess = ensureInnerSuccess;
|
|
5
|
+
exports.extractData = extractData;
|
|
6
|
+
exports.buildInnerUrl = buildInnerUrl;
|
|
7
|
+
const error_1 = require("../../utils/error");
|
|
8
|
+
/**
|
|
9
|
+
* 校验 dataloom InnerAPI 响应的 envelope。
|
|
10
|
+
*
|
|
11
|
+
* 真实响应结构:`{ data: {...业务字段...}, status_code: "0" }`
|
|
12
|
+
* - `status_code == "0"` 或缺省视为成功
|
|
13
|
+
* - 其他值视为业务错误,按业务 code 或 SQLSTATE 映射到 CLI code
|
|
14
|
+
*/
|
|
15
|
+
function ensureInnerSuccess(body) {
|
|
16
|
+
if (!body) {
|
|
17
|
+
throw new error_1.AppError("INTERNAL_DB_ERROR", "empty response body");
|
|
18
|
+
}
|
|
19
|
+
const code = body.status_code ?? body.ErrorCode ?? "0";
|
|
20
|
+
if (code === "0" || code === "")
|
|
21
|
+
return;
|
|
22
|
+
const message = stripPgPrefix(body.error_msg ?? body.ErrorMessage ?? `dataloom API error [${code}]`);
|
|
23
|
+
// k_dl_1300002 是 PG 执行透传错误;error_msg 里常带 SQLSTATE,优先按 SQLSTATE 映射
|
|
24
|
+
if (code === "k_dl_1300002") {
|
|
25
|
+
const sqlstate = extractSqlstate(message);
|
|
26
|
+
if (sqlstate && exports.SQLSTATE_MAP[sqlstate]) {
|
|
27
|
+
throw new error_1.AppError(exports.SQLSTATE_MAP[sqlstate], message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// k_dl_1600000 是 dataloom 通用参数错误,不能整体映射;
|
|
31
|
+
// message 以 "Invalid DB Branch" 开头的两种触发场景:
|
|
32
|
+
// 1) 单环境应用使用 --env(多环境未初始化)
|
|
33
|
+
// 2) 多环境应用传入了不存在的 env 名(如 --env staging)
|
|
34
|
+
// 两者外部表象一致:dbBranch 在 db_branch 表里查不到,固定映射为 MULTI_ENV_NOT_INITIALIZED。
|
|
35
|
+
if (code === "k_dl_1600000" && message.startsWith("Invalid DB Branch")) {
|
|
36
|
+
throw new error_1.AppError("MULTI_ENV_NOT_INITIALIZED", "--env is not available (multi-env not initialized)", {
|
|
37
|
+
next_actions: [
|
|
38
|
+
"Verify the --env value matches an existing dbBranch, or run `miaoda db migration init` to set up multi-env for this app.",
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// 业务 code 优先映射到语义 CLI code;未知 code 透传为 DB_API_<code> 样式
|
|
43
|
+
const mapped = BIZ_ERR_MAP.get(code);
|
|
44
|
+
if (mapped) {
|
|
45
|
+
throw new error_1.AppError(mapped.code, mapped.message ?? message, {
|
|
46
|
+
next_actions: mapped.hint ? [mapped.hint] : undefined,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// 兜底:dataloom 未映射的 code 原样透传
|
|
50
|
+
throw new error_1.AppError(`DB_API_${code}`, message);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 剥掉 PG 透传错误的 "ERROR:" 前缀:CLI 输出本身就会前缀 "Error:",
|
|
54
|
+
* 不去掉就会变成 `Error: ERROR: relation ...` 双重前缀,PRD 不要这种冗余。
|
|
55
|
+
* 大小写敏感,只匹配 PG 标准格式。
|
|
56
|
+
*/
|
|
57
|
+
function stripPgPrefix(msg) {
|
|
58
|
+
return msg.replace(/^ERROR:\s*/, "");
|
|
59
|
+
}
|
|
60
|
+
/** 从 PG 执行错误消息里提取 "(SQLSTATE XXXXX)"。 */
|
|
61
|
+
function extractSqlstate(msg) {
|
|
62
|
+
const m = /SQLSTATE\s+([0-9A-Z]{5})/.exec(msg);
|
|
63
|
+
return m?.[1];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* dataloom 业务错误码 → CLI 语义化 code 映射。
|
|
67
|
+
*
|
|
68
|
+
* 长期维护,观察到新 code 在此增补。
|
|
69
|
+
* 常见 dataloom 错误:详见 `common/dataloom_error/` 错误码定义(范围 `k_dl_*xxxxx`)。
|
|
70
|
+
*/
|
|
71
|
+
const BIZ_ERR_MAP = new Map(Object.entries({
|
|
72
|
+
// 来源:真实环境探测(dataloom InnerExecuteSQL / InnerGetSchema)
|
|
73
|
+
// k_dl_000001:DB 连接 / QPS 限流 等基础设施层错误
|
|
74
|
+
"k_dl_000001": { code: "INTERNAL_DB_ERROR", hint: "检查 dbBranch 与应用 PG 实例状态,或稍后重试" },
|
|
75
|
+
// k_dl_000002:SQL 解析 / 不支持的 SQL 类型(VACUUM、COPY、SET、SHOW 等)
|
|
76
|
+
"k_dl_000002": { code: "SQL_SYNTAX_ERROR" },
|
|
77
|
+
// k_dl_000003:查询命中系统表(pg_tables / pg_user 等)被拒
|
|
78
|
+
"k_dl_000003": { code: "SQL_OPERATION_FORBIDDEN" },
|
|
79
|
+
// k_dl_1300002:PG 执行错误;实际 SQLSTATE 由 extractSqlstate 单独映射
|
|
80
|
+
// 未匹配到 SQLSTATE 时走下面兜底 DB_API_<code>
|
|
81
|
+
}));
|
|
82
|
+
/** PG SQLSTATE → CLI code(当前 dataloom 不一定透传,预留未来使用) */
|
|
83
|
+
exports.SQLSTATE_MAP = {
|
|
84
|
+
"42601": "SQL_SYNTAX_ERROR",
|
|
85
|
+
"42P01": "TABLE_NOT_FOUND",
|
|
86
|
+
"42703": "COLUMN_NOT_FOUND",
|
|
87
|
+
"57014": "STATEMENT_TIMEOUT",
|
|
88
|
+
"23505": "UNIQUE_VIOLATION",
|
|
89
|
+
"22P02": "TYPE_MISMATCH",
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* 从 envelope 里取 data 业务字段,校验成功后返回;失败则 throw。
|
|
93
|
+
*/
|
|
94
|
+
function extractData(body) {
|
|
95
|
+
ensureInnerSuccess(body);
|
|
96
|
+
if (!body?.data) {
|
|
97
|
+
throw new error_1.AppError("INTERNAL_DB_ERROR", "response missing data field");
|
|
98
|
+
}
|
|
99
|
+
return body.data;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 组装 `/v1/dataloom/app/{appId}/...` URL(admin-inner 链路)。
|
|
103
|
+
*
|
|
104
|
+
* 后端 IDL 已把 dataloom 的 4 个 inner 接口切到管理态(ForceAdminKctx),
|
|
105
|
+
* 路径前缀也统一改为 `/v1/dataloom/app/:appID/...`:
|
|
106
|
+
* - sql: POST /v1/dataloom/app/{appId}/db/sql
|
|
107
|
+
* - schema: GET /v1/dataloom/app/{appId}/db/schema
|
|
108
|
+
* - import: POST /v1/dataloom/app/{appId}/data/import
|
|
109
|
+
* - export: POST /v1/dataloom/app/{appId}/data/export
|
|
110
|
+
* 调用方传入的 path 已包含 `/db` / `/data` 中段,这里只负责拼前缀和 query。
|
|
111
|
+
*/
|
|
112
|
+
function buildInnerUrl(appId, path, query) {
|
|
113
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
114
|
+
let url = `/v1/dataloom/app/${encodeURIComponent(appId)}${normalized}`;
|
|
115
|
+
if (query) {
|
|
116
|
+
const usp = new URLSearchParams();
|
|
117
|
+
for (const [k, v] of Object.entries(query)) {
|
|
118
|
+
if (v !== undefined && v !== "")
|
|
119
|
+
usp.append(k, v);
|
|
120
|
+
}
|
|
121
|
+
const qs = usp.toString();
|
|
122
|
+
if (qs)
|
|
123
|
+
url += `?${qs}`;
|
|
124
|
+
}
|
|
125
|
+
return url;
|
|
126
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pickTableDetail = exports.flattenSchemaList = exports.parseSqlResult = exports.SQLSTATE_MAP = exports.ensureInnerSuccess = exports.buildInnerUrl = exports.exportData = exports.importData = exports.getSchema = exports.execSql = void 0;
|
|
4
|
+
var api_1 = require("./api");
|
|
5
|
+
Object.defineProperty(exports, "execSql", { enumerable: true, get: function () { return api_1.execSql; } });
|
|
6
|
+
Object.defineProperty(exports, "getSchema", { enumerable: true, get: function () { return api_1.getSchema; } });
|
|
7
|
+
Object.defineProperty(exports, "importData", { enumerable: true, get: function () { return api_1.importData; } });
|
|
8
|
+
Object.defineProperty(exports, "exportData", { enumerable: true, get: function () { return api_1.exportData; } });
|
|
9
|
+
var client_1 = require("./client");
|
|
10
|
+
Object.defineProperty(exports, "buildInnerUrl", { enumerable: true, get: function () { return client_1.buildInnerUrl; } });
|
|
11
|
+
Object.defineProperty(exports, "ensureInnerSuccess", { enumerable: true, get: function () { return client_1.ensureInnerSuccess; } });
|
|
12
|
+
Object.defineProperty(exports, "SQLSTATE_MAP", { enumerable: true, get: function () { return client_1.SQLSTATE_MAP; } });
|
|
13
|
+
var parsers_1 = require("./parsers");
|
|
14
|
+
Object.defineProperty(exports, "parseSqlResult", { enumerable: true, get: function () { return parsers_1.parseSqlResult; } });
|
|
15
|
+
Object.defineProperty(exports, "flattenSchemaList", { enumerable: true, get: function () { return parsers_1.flattenSchemaList; } });
|
|
16
|
+
Object.defineProperty(exports, "pickTableDetail", { enumerable: true, get: function () { return parsers_1.pickTableDetail; } });
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseSqlResult = parseSqlResult;
|
|
4
|
+
exports.flattenSchemaList = flattenSchemaList;
|
|
5
|
+
exports.pickTableDetail = pickTableDetail;
|
|
6
|
+
const error_1 = require("../../utils/error");
|
|
7
|
+
// ── SQL 结果 ──
|
|
8
|
+
/**
|
|
9
|
+
* 解析 InnerExecuteSQL 的单条 results[](PRD 要求多条语句只取最后一条)。
|
|
10
|
+
*/
|
|
11
|
+
function parseSqlResult(r) {
|
|
12
|
+
if (r.sqlType === "SELECT") {
|
|
13
|
+
let rows = [];
|
|
14
|
+
if (r.data) {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(r.data);
|
|
17
|
+
if (Array.isArray(parsed))
|
|
18
|
+
rows = parsed;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new error_1.AppError("INTERNAL_DB_ERROR", "Failed to parse SELECT result JSON");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
kind: "select",
|
|
26
|
+
rows,
|
|
27
|
+
recordCount: r.recordCount ?? rows.length,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (r.sqlType === "INSERT" || r.sqlType === "UPDATE" || r.sqlType === "DELETE" || r.sqlType === "DML") {
|
|
31
|
+
const affected = r.affectedRows ?? extractRowCount(r.data);
|
|
32
|
+
return {
|
|
33
|
+
kind: "dml",
|
|
34
|
+
sqlType: r.sqlType,
|
|
35
|
+
affectedRows: affected,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// DDL or unknown
|
|
39
|
+
return { kind: "ddl" };
|
|
40
|
+
}
|
|
41
|
+
/** DML 的 data 通常是 `[{"rowCount": N}]`;兜底从这里读影响行数。 */
|
|
42
|
+
function extractRowCount(data) {
|
|
43
|
+
if (!data)
|
|
44
|
+
return 0;
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(data);
|
|
47
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
48
|
+
const first = parsed[0];
|
|
49
|
+
const v = first.rowCount ?? first.affected_rows;
|
|
50
|
+
if (typeof v === "number")
|
|
51
|
+
return v;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// ignore
|
|
56
|
+
}
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
// ── schema ──
|
|
60
|
+
/**
|
|
61
|
+
* 合并 tables / views / materializedViews 为扁平 TableSummary[](schema list 视图)。
|
|
62
|
+
* 统计信息(estimated_row_count / size_bytes)来自 `root.tableStats[tableName]`,
|
|
63
|
+
* 需请求带 `includeStats=true` 才会有。
|
|
64
|
+
*/
|
|
65
|
+
function flattenSchemaList(root) {
|
|
66
|
+
if (!root)
|
|
67
|
+
return [];
|
|
68
|
+
const statsMap = root.tableStats ?? {};
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const t of root.tables?.data ?? [])
|
|
71
|
+
out.push(toSummary(t, statsMap[t.tableName]));
|
|
72
|
+
for (const v of root.views?.data ?? [])
|
|
73
|
+
out.push(toSummary(v, statsMap[v.tableName]));
|
|
74
|
+
for (const mv of root.materializedViews?.data ?? [])
|
|
75
|
+
out.push(toSummary(mv, statsMap[mv.tableName]));
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function toSummary(t, stats) {
|
|
79
|
+
return {
|
|
80
|
+
name: t.tableName,
|
|
81
|
+
description: t.comment && t.comment.length > 0 ? t.comment : null,
|
|
82
|
+
columns: (t.fields ?? []).length,
|
|
83
|
+
estimated_row_count: typeof stats?.estimatedRowCount === "number" ? stats.estimatedRowCount : null,
|
|
84
|
+
size_bytes: typeof stats?.sizeBytes === "number" ? stats.sizeBytes : null,
|
|
85
|
+
updated_at: t.updatedAt,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 从 InnerSchemaRespVO 里定位单表 → 转 CLI TableDetail。
|
|
90
|
+
* 依次查 tables / views / materializedViews;找不到返回 null。
|
|
91
|
+
*/
|
|
92
|
+
function pickTableDetail(root, tableName) {
|
|
93
|
+
if (!root)
|
|
94
|
+
return null;
|
|
95
|
+
const stats = root.tableStats?.[tableName];
|
|
96
|
+
const pools = [
|
|
97
|
+
root.tables?.data,
|
|
98
|
+
root.views?.data,
|
|
99
|
+
root.materializedViews?.data,
|
|
100
|
+
];
|
|
101
|
+
for (const arr of pools) {
|
|
102
|
+
if (!arr)
|
|
103
|
+
continue;
|
|
104
|
+
const hit = arr.find((t) => t.tableName === tableName);
|
|
105
|
+
if (hit)
|
|
106
|
+
return toDetail(hit, stats);
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
function toDetail(t, stats) {
|
|
111
|
+
// 优先用 tableStats.indexes(PRIMARY + UNIQUE + INDEX 统一视图);
|
|
112
|
+
// 回退到 TableVO.indexes(仅二级索引,老后端没返回 tableStats 时使用)
|
|
113
|
+
const rawIndexes = stats?.indexes ?? t.indexes ?? [];
|
|
114
|
+
return {
|
|
115
|
+
name: t.tableName,
|
|
116
|
+
description: t.comment && t.comment.length > 0 ? t.comment : null,
|
|
117
|
+
columns: (t.fields ?? []).map(toColumn),
|
|
118
|
+
indexes: rawIndexes.map(toIndex),
|
|
119
|
+
estimated_row_count: typeof stats?.estimatedRowCount === "number" ? stats.estimatedRowCount : null,
|
|
120
|
+
size_bytes: typeof stats?.sizeBytes === "number" ? stats.sizeBytes : null,
|
|
121
|
+
created_at: t.createdAt,
|
|
122
|
+
updated_at: t.updatedAt,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function toColumn(f) {
|
|
126
|
+
const type = f.arrayElementType ? `${f.arrayElementType}[]` : f.type;
|
|
127
|
+
return {
|
|
128
|
+
name: f.fieldName,
|
|
129
|
+
type,
|
|
130
|
+
nullable: f.isNullable ?? false,
|
|
131
|
+
default: f.defaultValue ?? null,
|
|
132
|
+
// 后端 comment *string 在没有 comment 时可能返 "" 或 null,这里统一归一为 null(对齐 PRD)
|
|
133
|
+
comment: f.comment && f.comment.length > 0 ? f.comment : null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function toIndex(i) {
|
|
137
|
+
return {
|
|
138
|
+
name: i.indexName,
|
|
139
|
+
type: translateIndexType(i.indexType),
|
|
140
|
+
columns: i.indexColumns ?? [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 把后端原始 IndexType(小写:primary / unique / normal / foreign)翻译为 PRD 约定的展示值。
|
|
145
|
+
* 规则:
|
|
146
|
+
* primary → "PRIMARY KEY"
|
|
147
|
+
* unique → "UNIQUE"
|
|
148
|
+
* 其它 → "INDEX"(foreign 外键通过 relationships 表达,不进 indexes;真的遇到也归 INDEX)
|
|
149
|
+
*/
|
|
150
|
+
function translateIndexType(raw) {
|
|
151
|
+
const s = raw.toLowerCase();
|
|
152
|
+
if (s === "primary")
|
|
153
|
+
return "PRIMARY KEY";
|
|
154
|
+
if (s === "unique")
|
|
155
|
+
return "UNIQUE";
|
|
156
|
+
return "INDEX";
|
|
157
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ── dataloom InnerAPI 响应信封 ──
|
|
3
|
+
//
|
|
4
|
+
// 实际响应结构(与 file-storage 类似):
|
|
5
|
+
// { "data": { ...业务字段... }, "status_code": "0", "error_msg": "..." }
|
|
6
|
+
//
|
|
7
|
+
// - `status_code == "0"` 成功
|
|
8
|
+
// - `status_code != "0"` 业务错误(如 PG SQLSTATE 映射后的 code)
|
|
9
|
+
// - 业务字段统一在 `data` 下:`results` / `schema` / `ddl` 等
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|