@lark-apaas/miaoda-cli 0.1.0 → 0.1.1
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 +211 -0
- package/dist/api/db/client.js +166 -0
- package/dist/api/db/index.js +16 -0
- package/dist/api/db/parsers.js +161 -0
- package/dist/api/db/types.js +10 -0
- package/dist/api/file/api.js +467 -0
- package/dist/api/file/client.js +199 -0
- package/dist/api/file/detect.js +56 -0
- package/dist/api/file/index.js +18 -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 +208 -0
- package/dist/cli/commands/file/index.js +212 -0
- package/dist/cli/commands/index.js +4 -0
- package/dist/cli/commands/plugin/index.js +2 -1
- package/dist/cli/commands/shared.js +7 -8
- package/dist/cli/handlers/db/data.js +171 -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 +367 -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 +111 -0
- package/dist/cli/handlers/file/rm.js +263 -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/cli/help.js +188 -0
- package/dist/main.js +9 -1
- package/dist/utils/error.js +3 -0
- package/dist/utils/http.js +31 -10
- package/dist/utils/index.js +3 -1
- package/dist/utils/output.js +18 -5
- package/dist/utils/render.js +187 -0
- package/package.json +2 -2
|
@@ -0,0 +1,211 @@
|
|
|
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 http_client_1 = require("@lark-apaas/http-client");
|
|
10
|
+
const client_1 = require("./client");
|
|
11
|
+
/**
|
|
12
|
+
* 把 SDK 抛出的 HttpError 统一映射成 CLI 层错误:
|
|
13
|
+
* 1. 先尝试从 response body 解 envelope,命中 dataloom 业务 code → AppError
|
|
14
|
+
* 2. 兜底返 HttpError,保留真实 status 码与上下文
|
|
15
|
+
*
|
|
16
|
+
* 配合调用点的 try/catch + traceHttp,让 --verbose 在错误路径上也能拿到
|
|
17
|
+
* x-tt-logid 与 status,方便定位线上问题。
|
|
18
|
+
*/
|
|
19
|
+
async function mapDbHttpError(err, url, ctx) {
|
|
20
|
+
if (err instanceof error_1.AppError)
|
|
21
|
+
throw err;
|
|
22
|
+
if (err instanceof http_client_1.HttpError) {
|
|
23
|
+
const status = err.response?.status ?? 0;
|
|
24
|
+
const statusText = err.response?.statusText ?? "";
|
|
25
|
+
try {
|
|
26
|
+
const body = (await err.response?.json());
|
|
27
|
+
if (body)
|
|
28
|
+
(0, client_1.extractData)(body); // 业务 code 命中 → 抛 AppError;不命中走兜底
|
|
29
|
+
}
|
|
30
|
+
catch (innerErr) {
|
|
31
|
+
if (innerErr instanceof error_1.AppError)
|
|
32
|
+
throw innerErr;
|
|
33
|
+
// body 解析失败 → 当成无 envelope 的纯 HTTP 错误处理
|
|
34
|
+
}
|
|
35
|
+
throw new error_1.HttpError(status, url, `${ctx}: ${String(status)} ${statusText}`.trim());
|
|
36
|
+
}
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
// CLI 不再为 dbBranch 设默认值:
|
|
40
|
+
// 用户没传 --env 就完全不携带 dbBranch query 参数,由后端 admin-inner 中间件
|
|
41
|
+
// 按 workspace 多环境状态决定(多环境 → dev / 单环境 → main)。
|
|
42
|
+
// 这样可以避免单环境应用被强行打到 main 之外、或多环境应用被默认打到 main 上线库。
|
|
43
|
+
// ── db sql → InnerAdminExecuteSQL ──
|
|
44
|
+
/**
|
|
45
|
+
* 执行 SQL(admin-inner)。
|
|
46
|
+
* 后端:POST /v1/dataloom/app/{appId}/db/sql?dbBranch=main
|
|
47
|
+
*
|
|
48
|
+
* 返回所有 results[](多条语句时每条一项);CLI 侧按 PRD 仅取最后一条展示,
|
|
49
|
+
* 但 API 层保留完整列表以便测试和高级用法。
|
|
50
|
+
*/
|
|
51
|
+
async function execSql(opts) {
|
|
52
|
+
const client = (0, http_1.getHttpClient)();
|
|
53
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/sql", {
|
|
54
|
+
dbBranch: opts.dbBranch,
|
|
55
|
+
});
|
|
56
|
+
const start = Date.now();
|
|
57
|
+
let response;
|
|
58
|
+
try {
|
|
59
|
+
response = await client.post(url, { sql: opts.sql });
|
|
60
|
+
(0, client_1.traceHttp)("POST", url, start, response);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
(0, client_1.traceHttp)("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
|
|
64
|
+
await mapDbHttpError(err, url, "Failed to execute SQL");
|
|
65
|
+
throw err; // 不可达
|
|
66
|
+
}
|
|
67
|
+
const body = (await response.json());
|
|
68
|
+
const data = (0, client_1.extractData)(body);
|
|
69
|
+
return data.results ?? [];
|
|
70
|
+
}
|
|
71
|
+
// ── db schema → InnerGetSchema ──
|
|
72
|
+
/**
|
|
73
|
+
* 查询 schema(admin-inner)。
|
|
74
|
+
* 后端:GET /v1/dataloom/app/{appId}/db/schema?format=schema|ddl&tableNames=...&includeStats=...&dbBranch=main
|
|
75
|
+
*
|
|
76
|
+
* 返回 body.data(含 `schema` 或 `ddl`)供 handler 使用。
|
|
77
|
+
*/
|
|
78
|
+
async function getSchema(opts) {
|
|
79
|
+
const client = (0, http_1.getHttpClient)();
|
|
80
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/schema", {
|
|
81
|
+
format: opts.format ?? "schema",
|
|
82
|
+
tableNames: opts.tableNames,
|
|
83
|
+
includeStats: opts.includeStats ? "true" : undefined,
|
|
84
|
+
dbBranch: opts.dbBranch,
|
|
85
|
+
});
|
|
86
|
+
const start = Date.now();
|
|
87
|
+
let response;
|
|
88
|
+
try {
|
|
89
|
+
response = await client.get(url);
|
|
90
|
+
(0, client_1.traceHttp)("GET", url, start, response);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
(0, client_1.traceHttp)("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
|
|
94
|
+
await mapDbHttpError(err, url, "Failed to get schema");
|
|
95
|
+
throw err; // 不可达
|
|
96
|
+
}
|
|
97
|
+
const body = (await response.json());
|
|
98
|
+
return (0, client_1.extractData)(body);
|
|
99
|
+
}
|
|
100
|
+
// ── db data import → InnerAdminImportData ──
|
|
101
|
+
/**
|
|
102
|
+
* 导入文件。
|
|
103
|
+
* 后端:POST /v1/dataloom/app/{appId}/data/import
|
|
104
|
+
*
|
|
105
|
+
* 全字段走 JSON body envelope(idl-larkgw 36125a2f):
|
|
106
|
+
* {tableName, format, records, dbBranch?}
|
|
107
|
+
*
|
|
108
|
+
* `records` 字段携带 CSV / JSON 文本内容(utf8 字符串),与 dataloom 上
|
|
109
|
+
* Import/ExportAdminRecords 命名风格对齐。CLI 端把 Buffer 解码成 utf8
|
|
110
|
+
* 字符串后塞进 envelope 即可。
|
|
111
|
+
*/
|
|
112
|
+
async function importData(opts) {
|
|
113
|
+
const client = (0, http_1.getHttpClient)();
|
|
114
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import");
|
|
115
|
+
const reqBody = {
|
|
116
|
+
tableName: opts.tableName,
|
|
117
|
+
format: opts.format,
|
|
118
|
+
records: opts.body.toString("utf8"),
|
|
119
|
+
};
|
|
120
|
+
if (opts.dbBranch !== undefined && opts.dbBranch !== "") {
|
|
121
|
+
reqBody.dbBranch = opts.dbBranch;
|
|
122
|
+
}
|
|
123
|
+
const start = Date.now();
|
|
124
|
+
let response;
|
|
125
|
+
try {
|
|
126
|
+
response = await client.post(url, reqBody);
|
|
127
|
+
(0, client_1.traceHttp)("POST", url, start, response);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
(0, client_1.traceHttp)("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
|
|
131
|
+
await mapDbHttpError(err, url, "Failed to import data");
|
|
132
|
+
throw err; // 不可达
|
|
133
|
+
}
|
|
134
|
+
// 后端 InnerAdminImportData 响应里 data 直接返 {tableName, recordCount, durationMs}
|
|
135
|
+
const body = (await response.json());
|
|
136
|
+
const data = (0, client_1.extractData)(body);
|
|
137
|
+
return {
|
|
138
|
+
tableName: data.tableName ?? opts.tableName,
|
|
139
|
+
recordCount: data.recordCount ?? 0,
|
|
140
|
+
durationMs: data.durationMs ?? 0,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// ── db data export → InnerAdminExportData ──
|
|
144
|
+
/**
|
|
145
|
+
* 导出数据。
|
|
146
|
+
* 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&limit=5000&dbBranch=main
|
|
147
|
+
*
|
|
148
|
+
* 所有参数(含 limit)均按 IDL `api.query` 走 query string;HTTP 方法是 POST(对齐
|
|
149
|
+
* inner_api 网关插件路由约定,与 InnerAdminExecuteSQL 同 method)。请求体为空。
|
|
150
|
+
* 响应 body 为原始 CSV/JSON 字节,RecordCount 通过响应头 `X-Miaoda-Record-Count`
|
|
151
|
+
* 回传,错误仍走 HTTP 4xx/5xx + envelope。
|
|
152
|
+
*/
|
|
153
|
+
async function exportData(opts) {
|
|
154
|
+
const client = (0, http_1.getHttpClient)();
|
|
155
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/export", {
|
|
156
|
+
tableName: opts.tableName,
|
|
157
|
+
format: opts.format,
|
|
158
|
+
limit: String(opts.limit ?? 5000),
|
|
159
|
+
dbBranch: opts.dbBranch,
|
|
160
|
+
});
|
|
161
|
+
// POST + 空 body:所有业务参数都在 query 里
|
|
162
|
+
const start = Date.now();
|
|
163
|
+
let response;
|
|
164
|
+
try {
|
|
165
|
+
response = await client.request({ method: "POST", url });
|
|
166
|
+
(0, client_1.traceHttp)("POST", url, start, response);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
(0, client_1.traceHttp)("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
|
|
170
|
+
await mapDbHttpError(err, url, "Failed to export data");
|
|
171
|
+
throw err; // 不可达
|
|
172
|
+
}
|
|
173
|
+
// 成功路径:响应 body 通常是原始 CSV/JSON 字节,但部分错误场景下网关会返
|
|
174
|
+
// HTTP 200 + JSON envelope(status_code != "0"),需要在这里嗅探兜底。
|
|
175
|
+
const contentType = response.headers.get("Content-Type") ??
|
|
176
|
+
(opts.format === "csv" ? "text/csv" : "application/json");
|
|
177
|
+
const ab = await response.arrayBuffer();
|
|
178
|
+
const buf = Buffer.from(new Uint8Array(ab));
|
|
179
|
+
if (buf.length === 0) {
|
|
180
|
+
throw new error_1.AppError("INTERNAL_DB_ERROR", "Empty export response body");
|
|
181
|
+
}
|
|
182
|
+
// Envelope sniff:HTTP 200 + Content-Type 为 application/json + body 解析得到
|
|
183
|
+
// InnerEnvelope 且 status_code 非 "0" 时,按业务错误抛出。
|
|
184
|
+
// 仅 CSV 格式做 sniff —— JSON 格式正常成功响应也是 application/json,会误判。
|
|
185
|
+
if (opts.format === "csv" && /application\/json/i.test(contentType)) {
|
|
186
|
+
try {
|
|
187
|
+
const parsed = JSON.parse(buf.toString("utf8"));
|
|
188
|
+
if (parsed.status_code != null && parsed.status_code !== "0") {
|
|
189
|
+
// 复用 extractData 的错误映射逻辑(throw AppError)
|
|
190
|
+
(0, client_1.extractData)(parsed);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
// 已经被 extractData 抛成 AppError → 透传;否则 JSON.parse 失败说明 body
|
|
195
|
+
// 真的是 CSV 文本,继续按成功流程走
|
|
196
|
+
if (err instanceof error_1.AppError)
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// 后端通过响应头回传记录数(避免污染 body);header 缺失或解析失败 → undefined
|
|
201
|
+
const recordCountHeader = response.headers.get("X-Miaoda-Record-Count");
|
|
202
|
+
const parsedCount = recordCountHeader != null ? Number(recordCountHeader) : NaN;
|
|
203
|
+
const recordCount = Number.isFinite(parsedCount) && parsedCount >= 0 ? parsedCount : undefined;
|
|
204
|
+
return {
|
|
205
|
+
tableName: opts.tableName,
|
|
206
|
+
format: opts.format,
|
|
207
|
+
contentType,
|
|
208
|
+
body: buf,
|
|
209
|
+
recordCount,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SQLSTATE_MAP = void 0;
|
|
4
|
+
exports.traceHttp = traceHttp;
|
|
5
|
+
exports.ensureInnerSuccess = ensureInnerSuccess;
|
|
6
|
+
exports.extractData = extractData;
|
|
7
|
+
exports.buildInnerUrl = buildInnerUrl;
|
|
8
|
+
const error_1 = require("../../utils/error");
|
|
9
|
+
const logger_1 = require("../../utils/logger");
|
|
10
|
+
/**
|
|
11
|
+
* 输出一条 HTTP 调试日志(仅 --verbose 模式生效)。
|
|
12
|
+
*
|
|
13
|
+
* 主要用于把后端返回的 `x-tt-logid` 透出给用户,方便拿这个 id 去 server / 网关日志里
|
|
14
|
+
* 直接定位本次请求的 `[MiaodaCLI.metric]` 行与上下游 trace。
|
|
15
|
+
*
|
|
16
|
+
* 使用约定:
|
|
17
|
+
* const start = Date.now();
|
|
18
|
+
* const response = await client.post(url, body);
|
|
19
|
+
* traceHttp("POST", url, start, response);
|
|
20
|
+
* // 或错误路径:traceHttp("POST", url, start, err.response, err)
|
|
21
|
+
*/
|
|
22
|
+
function traceHttp(method, url, start, response, err) {
|
|
23
|
+
// debug() 内部已判断 verbose 开关,这里不重复判断;保持调用点轻量
|
|
24
|
+
try {
|
|
25
|
+
const cost = Date.now() - start;
|
|
26
|
+
const status = response?.status ?? 0;
|
|
27
|
+
const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
|
|
28
|
+
if (err !== undefined) {
|
|
29
|
+
const errMsg = err instanceof Error
|
|
30
|
+
? err.message
|
|
31
|
+
: typeof err === "string"
|
|
32
|
+
? err
|
|
33
|
+
: JSON.stringify(err);
|
|
34
|
+
(0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
(0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid}`);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// debug 失败不应影响业务,吞掉
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 校验 dataloom InnerAPI 响应的 envelope。
|
|
45
|
+
*
|
|
46
|
+
* 真实响应结构:`{ data: {...业务字段...}, status_code: "0" }`
|
|
47
|
+
* - `status_code == "0"` 或缺省视为成功
|
|
48
|
+
* - 其他值视为业务错误,按业务 code 或 SQLSTATE 映射到 CLI code
|
|
49
|
+
*/
|
|
50
|
+
function ensureInnerSuccess(body) {
|
|
51
|
+
if (!body) {
|
|
52
|
+
throw new error_1.AppError("INTERNAL_DB_ERROR", "empty response body");
|
|
53
|
+
}
|
|
54
|
+
const code = body.status_code ?? body.ErrorCode ?? "0";
|
|
55
|
+
if (code === "0" || code === "")
|
|
56
|
+
return;
|
|
57
|
+
const message = stripPgPrefix(body.error_msg ?? body.ErrorMessage ?? `dataloom API error [${code}]`);
|
|
58
|
+
// PRD 多语句失败:后端在 envelope 顶层透出 errorStatementIndex(从 0 起计),
|
|
59
|
+
// 单语句 / 单元执行不会带这个字段。下面的 AppError 都把它带上,让最终
|
|
60
|
+
// CLI JSON envelope 写到 error.statement_index。
|
|
61
|
+
const stmtIdx = typeof body.errorStatementIndex === "number" ? body.errorStatementIndex : undefined;
|
|
62
|
+
// k_dl_1300002 是 PG 执行透传错误;error_msg 里常带 SQLSTATE,优先按 SQLSTATE 映射
|
|
63
|
+
if (code === "k_dl_1300002") {
|
|
64
|
+
const sqlstate = extractSqlstate(message);
|
|
65
|
+
if (sqlstate && exports.SQLSTATE_MAP[sqlstate]) {
|
|
66
|
+
throw new error_1.AppError(exports.SQLSTATE_MAP[sqlstate], message, { statement_index: stmtIdx });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// k_dl_1600000 是 dataloom 通用参数错误,不能整体映射;
|
|
70
|
+
// message 以 "Invalid DB Branch" 开头的两种触发场景:
|
|
71
|
+
// 1) 单环境应用使用 --env(多环境未初始化)
|
|
72
|
+
// 2) 多环境应用传入了不存在的 env 名(如 --env staging)
|
|
73
|
+
// 两者外部表象一致:dbBranch 在 db_branch 表里查不到,固定映射为 MULTI_ENV_NOT_INITIALIZED。
|
|
74
|
+
if (code === "k_dl_1600000" && message.startsWith("Invalid DB Branch")) {
|
|
75
|
+
throw new error_1.AppError("MULTI_ENV_NOT_INITIALIZED", "--env is not available (multi-env not initialized)", {
|
|
76
|
+
next_actions: [
|
|
77
|
+
"Verify the --env value matches an existing dbBranch.",
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// 业务 code 优先映射到语义 CLI code;未知 code 透传为 DB_API_<code> 样式
|
|
82
|
+
const mapped = BIZ_ERR_MAP.get(code);
|
|
83
|
+
if (mapped) {
|
|
84
|
+
throw new error_1.AppError(mapped.code, mapped.message ?? message, {
|
|
85
|
+
next_actions: mapped.hint ? [mapped.hint] : undefined,
|
|
86
|
+
statement_index: stmtIdx,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// 兜底:dataloom 未映射的 code 原样透传
|
|
90
|
+
throw new error_1.AppError(`DB_API_${code}`, message, { statement_index: stmtIdx });
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 剥掉 PG 透传错误的 "ERROR:" 前缀:CLI 输出本身就会前缀 "Error:",
|
|
94
|
+
* 不去掉就会变成 `Error: ERROR: relation ...` 双重前缀,PRD 不要这种冗余。
|
|
95
|
+
* 大小写敏感,只匹配 PG 标准格式。
|
|
96
|
+
*/
|
|
97
|
+
function stripPgPrefix(msg) {
|
|
98
|
+
return msg.replace(/^ERROR:\s*/, "");
|
|
99
|
+
}
|
|
100
|
+
/** 从 PG 执行错误消息里提取 "(SQLSTATE XXXXX)"。 */
|
|
101
|
+
function extractSqlstate(msg) {
|
|
102
|
+
const m = /SQLSTATE\s+([0-9A-Z]{5})/.exec(msg);
|
|
103
|
+
return m?.[1];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* dataloom 业务错误码 → CLI 语义化 code 映射。
|
|
107
|
+
*
|
|
108
|
+
* 长期维护,观察到新 code 在此增补。
|
|
109
|
+
* 常见 dataloom 错误:详见 `common/dataloom_error/` 错误码定义(范围 `k_dl_*xxxxx`)。
|
|
110
|
+
*/
|
|
111
|
+
const BIZ_ERR_MAP = new Map(Object.entries({
|
|
112
|
+
// 来源:真实环境探测(dataloom InnerExecuteSQL / InnerGetSchema)
|
|
113
|
+
// k_dl_000001:DB 连接 / QPS 限流 等基础设施层错误
|
|
114
|
+
"k_dl_000001": { code: "INTERNAL_DB_ERROR", hint: "检查 dbBranch 与应用 PG 实例状态,或稍后重试" },
|
|
115
|
+
// k_dl_000002:SQL 解析 / 不支持的 SQL 类型(VACUUM、COPY、SET、SHOW 等)
|
|
116
|
+
"k_dl_000002": { code: "SQL_SYNTAX_ERROR" },
|
|
117
|
+
// k_dl_000003:查询命中系统表(pg_tables / pg_user 等)被拒
|
|
118
|
+
"k_dl_000003": { code: "SQL_OPERATION_FORBIDDEN" },
|
|
119
|
+
// k_dl_1300002:PG 执行错误;实际 SQLSTATE 由 extractSqlstate 单独映射
|
|
120
|
+
// 未匹配到 SQLSTATE 时走下面兜底 DB_API_<code>
|
|
121
|
+
}));
|
|
122
|
+
/** PG SQLSTATE → CLI code(当前 dataloom 不一定透传,预留未来使用) */
|
|
123
|
+
exports.SQLSTATE_MAP = {
|
|
124
|
+
"42601": "SQL_SYNTAX_ERROR",
|
|
125
|
+
"42P01": "TABLE_NOT_FOUND",
|
|
126
|
+
"42703": "COLUMN_NOT_FOUND",
|
|
127
|
+
"57014": "STATEMENT_TIMEOUT",
|
|
128
|
+
"23505": "UNIQUE_VIOLATION",
|
|
129
|
+
"22P02": "TYPE_MISMATCH",
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* 从 envelope 里取 data 业务字段,校验成功后返回;失败则 throw。
|
|
133
|
+
*/
|
|
134
|
+
function extractData(body) {
|
|
135
|
+
ensureInnerSuccess(body);
|
|
136
|
+
if (!body?.data) {
|
|
137
|
+
throw new error_1.AppError("INTERNAL_DB_ERROR", "response missing data field");
|
|
138
|
+
}
|
|
139
|
+
return body.data;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 组装 `/v1/dataloom/app/{appId}/...` URL(admin-inner 链路)。
|
|
143
|
+
*
|
|
144
|
+
* 后端 IDL 已把 dataloom 的 4 个 inner 接口切到管理态(ForceAdminKctx),
|
|
145
|
+
* 路径前缀也统一改为 `/v1/dataloom/app/:appID/...`:
|
|
146
|
+
* - sql: POST /v1/dataloom/app/{appId}/db/sql
|
|
147
|
+
* - schema: GET /v1/dataloom/app/{appId}/db/schema
|
|
148
|
+
* - import: POST /v1/dataloom/app/{appId}/data/import
|
|
149
|
+
* - export: POST /v1/dataloom/app/{appId}/data/export
|
|
150
|
+
* 调用方传入的 path 已包含 `/db` / `/data` 中段,这里只负责拼前缀和 query。
|
|
151
|
+
*/
|
|
152
|
+
function buildInnerUrl(appId, path, query) {
|
|
153
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
154
|
+
let url = `/v1/dataloom/app/${encodeURIComponent(appId)}${normalized}`;
|
|
155
|
+
if (query) {
|
|
156
|
+
const usp = new URLSearchParams();
|
|
157
|
+
for (const [k, v] of Object.entries(query)) {
|
|
158
|
+
if (v !== undefined && v !== "")
|
|
159
|
+
usp.append(k, v);
|
|
160
|
+
}
|
|
161
|
+
const qs = usp.toString();
|
|
162
|
+
if (qs)
|
|
163
|
+
url += `?${qs}`;
|
|
164
|
+
}
|
|
165
|
+
return url;
|
|
166
|
+
}
|
|
@@ -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,161 @@
|
|
|
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" ||
|
|
31
|
+
r.sqlType === "UPDATE" ||
|
|
32
|
+
r.sqlType === "DELETE" ||
|
|
33
|
+
r.sqlType === "MERGE" ||
|
|
34
|
+
r.sqlType === "DML") {
|
|
35
|
+
const affected = r.affectedRows ?? extractRowCount(r.data);
|
|
36
|
+
return {
|
|
37
|
+
kind: "dml",
|
|
38
|
+
// 上面已 narrow,这里 cast 是为了 SqlType 联合里的 (string & {}) 让 TS 无法
|
|
39
|
+
// 自动收窄到字面量集合,不影响运行时安全
|
|
40
|
+
sqlType: r.sqlType,
|
|
41
|
+
affectedRows: affected,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// DDL or unknown — sqlType 透传后端给的细粒度(CREATE_TABLE / DROP_TABLE / ...
|
|
45
|
+
// / 笼统 "DDL"),CLI JSON 输出直接当 command 用
|
|
46
|
+
return { kind: "ddl", sqlType: r.sqlType };
|
|
47
|
+
}
|
|
48
|
+
/** DML 的 data 通常是 `[{"rowCount": N}]`;兜底从这里读影响行数。 */
|
|
49
|
+
function extractRowCount(data) {
|
|
50
|
+
if (!data)
|
|
51
|
+
return 0;
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(data);
|
|
54
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
55
|
+
const first = parsed[0];
|
|
56
|
+
const v = first.rowCount ?? first.affected_rows;
|
|
57
|
+
if (typeof v === "number")
|
|
58
|
+
return v;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// ignore
|
|
63
|
+
}
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
// ── schema ──
|
|
67
|
+
/**
|
|
68
|
+
* 合并 tables / views / materializedViews 为扁平 TableSummary[](schema list 视图)。
|
|
69
|
+
* 统计信息(estimated_row_count / size_bytes)来自 `root.tableStats[tableName]`,
|
|
70
|
+
* 需请求带 `includeStats=true` 才会有。
|
|
71
|
+
*/
|
|
72
|
+
function flattenSchemaList(root) {
|
|
73
|
+
if (!root)
|
|
74
|
+
return [];
|
|
75
|
+
const statsMap = root.tableStats ?? {};
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const t of root.tables?.data ?? [])
|
|
78
|
+
out.push(toSummary(t, statsMap[t.tableName]));
|
|
79
|
+
for (const v of root.views?.data ?? [])
|
|
80
|
+
out.push(toSummary(v, statsMap[v.tableName]));
|
|
81
|
+
for (const mv of root.materializedViews?.data ?? [])
|
|
82
|
+
out.push(toSummary(mv, statsMap[mv.tableName]));
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
function toSummary(t, stats) {
|
|
86
|
+
return {
|
|
87
|
+
name: t.tableName,
|
|
88
|
+
description: t.comment && t.comment.length > 0 ? t.comment : null,
|
|
89
|
+
columns: (t.fields ?? []).length,
|
|
90
|
+
estimated_row_count: typeof stats?.estimatedRowCount === "number" ? stats.estimatedRowCount : null,
|
|
91
|
+
size_bytes: typeof stats?.sizeBytes === "number" ? stats.sizeBytes : null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 从 InnerSchemaRespVO 里定位单表 → 转 CLI TableDetail。
|
|
96
|
+
* 依次查 tables / views / materializedViews;找不到返回 null。
|
|
97
|
+
*/
|
|
98
|
+
function pickTableDetail(root, tableName) {
|
|
99
|
+
if (!root)
|
|
100
|
+
return null;
|
|
101
|
+
const stats = root.tableStats?.[tableName];
|
|
102
|
+
const pools = [
|
|
103
|
+
root.tables?.data,
|
|
104
|
+
root.views?.data,
|
|
105
|
+
root.materializedViews?.data,
|
|
106
|
+
];
|
|
107
|
+
for (const arr of pools) {
|
|
108
|
+
if (!arr)
|
|
109
|
+
continue;
|
|
110
|
+
const hit = arr.find((t) => t.tableName === tableName);
|
|
111
|
+
if (hit)
|
|
112
|
+
return toDetail(hit, stats);
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
function toDetail(t, stats) {
|
|
117
|
+
// 优先用 tableStats.indexes(PRIMARY + UNIQUE + INDEX 统一视图);
|
|
118
|
+
// 回退到 TableVO.indexes(仅二级索引,老后端没返回 tableStats 时使用)
|
|
119
|
+
const rawIndexes = stats?.indexes ?? t.indexes ?? [];
|
|
120
|
+
return {
|
|
121
|
+
name: t.tableName,
|
|
122
|
+
description: t.comment && t.comment.length > 0 ? t.comment : null,
|
|
123
|
+
columns: (t.fields ?? []).map(toColumn),
|
|
124
|
+
indexes: rawIndexes.map(toIndex),
|
|
125
|
+
estimated_row_count: typeof stats?.estimatedRowCount === "number" ? stats.estimatedRowCount : null,
|
|
126
|
+
size_bytes: typeof stats?.sizeBytes === "number" ? stats.sizeBytes : null,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function toColumn(f) {
|
|
130
|
+
const type = f.arrayElementType ? `${f.arrayElementType}[]` : f.type;
|
|
131
|
+
return {
|
|
132
|
+
name: f.fieldName,
|
|
133
|
+
type,
|
|
134
|
+
nullable: f.isNullable ?? false,
|
|
135
|
+
default: f.defaultValue ?? null,
|
|
136
|
+
// 后端 comment *string 在没有 comment 时可能返 "" 或 null,这里统一归一为 null(对齐 PRD)
|
|
137
|
+
comment: f.comment && f.comment.length > 0 ? f.comment : null,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function toIndex(i) {
|
|
141
|
+
return {
|
|
142
|
+
name: i.indexName,
|
|
143
|
+
type: translateIndexType(i.indexType),
|
|
144
|
+
columns: i.indexColumns ?? [],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 把后端原始 IndexType(小写:primary / unique / normal / foreign)翻译为 PRD 约定的展示值。
|
|
149
|
+
* 规则:
|
|
150
|
+
* primary → "PRIMARY KEY"
|
|
151
|
+
* unique → "UNIQUE"
|
|
152
|
+
* 其它 → "INDEX"(foreign 外键通过 relationships 表达,不进 indexes;真的遇到也归 INDEX)
|
|
153
|
+
*/
|
|
154
|
+
function translateIndexType(raw) {
|
|
155
|
+
const s = raw.toLowerCase();
|
|
156
|
+
if (s === "primary")
|
|
157
|
+
return "PRIMARY KEY";
|
|
158
|
+
if (s === "unique")
|
|
159
|
+
return "UNIQUE";
|
|
160
|
+
return "INDEX";
|
|
161
|
+
}
|
|
@@ -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 });
|