@lark-apaas/miaoda-cli 0.1.0-alpha.097a394 → 0.1.0-alpha.0d863af
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 +133 -82
- package/dist/api/db/client.js +66 -8
- package/dist/api/db/parsers.js +10 -6
- package/dist/api/file/api.js +17 -12
- package/dist/api/file/client.js +49 -11
- package/dist/api/plugin/api.js +3 -3
- package/dist/cli/commands/db/index.js +135 -30
- package/dist/cli/commands/file/index.js +132 -35
- package/dist/cli/commands/plugin/index.js +2 -1
- package/dist/cli/commands/shared.js +7 -8
- package/dist/cli/handlers/db/data.js +8 -7
- package/dist/cli/handlers/db/schema.js +18 -16
- package/dist/cli/handlers/db/sql.js +115 -36
- package/dist/cli/handlers/file/cp.js +8 -8
- package/dist/cli/handlers/file/ls.js +11 -10
- package/dist/cli/handlers/file/rm.js +29 -9
- package/dist/cli/handlers/file/sign.js +3 -3
- package/dist/cli/handlers/file/stat.js +6 -6
- package/dist/cli/help.js +84 -0
- package/dist/main.js +8 -0
- 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/{cli/handlers/shared.js → utils/render.js} +66 -18
- package/package.json +2 -2
- package/dist/cli/handlers/file/helpers.js +0 -16
package/dist/api/db/api.js
CHANGED
|
@@ -6,35 +6,63 @@ exports.importData = importData;
|
|
|
6
6
|
exports.exportData = exportData;
|
|
7
7
|
const http_1 = require("../../utils/http");
|
|
8
8
|
const error_1 = require("../../utils/error");
|
|
9
|
+
const http_client_1 = require("@lark-apaas/http-client");
|
|
9
10
|
const client_1 = require("./client");
|
|
10
|
-
/** dataloom 默认 dbBranch —— 单环境应用均传 main */
|
|
11
|
-
const DEFAULT_DB_BRANCH = "main";
|
|
12
|
-
// ── db sql → InnerExecuteSQL ──
|
|
13
11
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
16
47
|
*
|
|
17
48
|
* 返回所有 results[](多条语句时每条一项);CLI 侧按 PRD 仅取最后一条展示,
|
|
18
49
|
* 但 API 层保留完整列表以便测试和高级用法。
|
|
19
50
|
*/
|
|
20
51
|
async function execSql(opts) {
|
|
21
52
|
const client = (0, http_1.getHttpClient)();
|
|
22
|
-
const url = (0, client_1.buildInnerUrl)(opts.appId, "/sql", {
|
|
23
|
-
dbBranch: opts.dbBranch
|
|
53
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/sql", {
|
|
54
|
+
dbBranch: opts.dbBranch,
|
|
24
55
|
});
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (body)
|
|
36
|
-
(0, client_1.extractData)(body);
|
|
37
|
-
throw new error_1.HttpError(response.status, url, `Failed to execute SQL: ${String(response.status)} ${response.statusText}`);
|
|
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; // 不可达
|
|
38
66
|
}
|
|
39
67
|
const body = (await response.json());
|
|
40
68
|
const data = (0, client_1.extractData)(body);
|
|
@@ -42,108 +70,108 @@ async function execSql(opts) {
|
|
|
42
70
|
}
|
|
43
71
|
// ── db schema → InnerGetSchema ──
|
|
44
72
|
/**
|
|
45
|
-
* 查询 schema
|
|
46
|
-
* 后端:GET /v1/app/{appId}/
|
|
73
|
+
* 查询 schema(admin-inner)。
|
|
74
|
+
* 后端:GET /v1/dataloom/app/{appId}/db/schema?format=schema|ddl&tableNames=...&includeStats=...&dbBranch=main
|
|
47
75
|
*
|
|
48
76
|
* 返回 body.data(含 `schema` 或 `ddl`)供 handler 使用。
|
|
49
77
|
*/
|
|
50
78
|
async function getSchema(opts) {
|
|
51
79
|
const client = (0, http_1.getHttpClient)();
|
|
52
|
-
const url = (0, client_1.buildInnerUrl)(opts.appId, "/schema", {
|
|
80
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/schema", {
|
|
53
81
|
format: opts.format ?? "schema",
|
|
54
82
|
tableNames: opts.tableNames,
|
|
55
83
|
includeStats: opts.includeStats ? "true" : undefined,
|
|
56
|
-
dbBranch: opts.dbBranch
|
|
84
|
+
dbBranch: opts.dbBranch,
|
|
57
85
|
});
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
(0, client_1.extractData)(body);
|
|
69
|
-
throw new error_1.HttpError(response.status, url, `Failed to get schema: ${String(response.status)} ${response.statusText}`);
|
|
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; // 不可达
|
|
70
96
|
}
|
|
71
97
|
const body = (await response.json());
|
|
72
98
|
return (0, client_1.extractData)(body);
|
|
73
99
|
}
|
|
74
|
-
// ── db data import →
|
|
100
|
+
// ── db data import → InnerAdminImportData ──
|
|
75
101
|
/**
|
|
76
102
|
* 导入文件。
|
|
77
|
-
* 后端:POST /v1/app/{appId}/
|
|
103
|
+
* 后端:POST /v1/dataloom/app/{appId}/data/import
|
|
104
|
+
*
|
|
105
|
+
* 全字段走 JSON body envelope(idl-larkgw 36125a2f):
|
|
106
|
+
* {tableName, format, records, dbBranch?}
|
|
78
107
|
*
|
|
79
|
-
*
|
|
108
|
+
* `records` 字段携带 CSV / JSON 文本内容(utf8 字符串),与 dataloom 上
|
|
109
|
+
* Import/ExportAdminRecords 命名风格对齐。CLI 端把 Buffer 解码成 utf8
|
|
110
|
+
* 字符串后塞进 envelope 即可。
|
|
80
111
|
*/
|
|
81
112
|
async function importData(opts) {
|
|
82
113
|
const client = (0, http_1.getHttpClient)();
|
|
83
|
-
const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import"
|
|
114
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import");
|
|
115
|
+
const reqBody = {
|
|
84
116
|
tableName: opts.tableName,
|
|
85
117
|
format: opts.format,
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
catch {
|
|
102
|
-
// ignore
|
|
103
|
-
}
|
|
104
|
-
if (body)
|
|
105
|
-
(0, client_1.extractData)(body);
|
|
106
|
-
throw new error_1.HttpError(response.status, url, `Failed to import data: ${String(response.status)} ${response.statusText}`);
|
|
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; // 不可达
|
|
107
133
|
}
|
|
108
|
-
// 后端
|
|
134
|
+
// 后端 InnerAdminImportData 响应里 data 直接返 {tableName, recordCount, durationMs}
|
|
109
135
|
const body = (await response.json());
|
|
110
136
|
const data = (0, client_1.extractData)(body);
|
|
111
137
|
return {
|
|
112
138
|
tableName: data.tableName ?? opts.tableName,
|
|
113
|
-
|
|
139
|
+
recordCount: data.recordCount ?? 0,
|
|
114
140
|
durationMs: data.durationMs ?? 0,
|
|
115
141
|
};
|
|
116
142
|
}
|
|
117
|
-
// ── db data export →
|
|
143
|
+
// ── db data export → InnerAdminExportData ──
|
|
118
144
|
/**
|
|
119
145
|
* 导出数据。
|
|
120
|
-
* 后端:POST /v1/app/{appId}/
|
|
146
|
+
* 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&limit=5000&dbBranch=main
|
|
121
147
|
*
|
|
122
|
-
*
|
|
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。
|
|
123
152
|
*/
|
|
124
153
|
async function exportData(opts) {
|
|
125
154
|
const client = (0, http_1.getHttpClient)();
|
|
126
155
|
const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/export", {
|
|
127
156
|
tableName: opts.tableName,
|
|
128
157
|
format: opts.format,
|
|
129
|
-
|
|
158
|
+
limit: String(opts.limit ?? 5000),
|
|
159
|
+
dbBranch: opts.dbBranch,
|
|
130
160
|
});
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (body)
|
|
143
|
-
(0, client_1.extractData)(body);
|
|
144
|
-
throw new error_1.HttpError(response.status, url, `Failed to export data: ${String(response.status)} ${response.statusText}`);
|
|
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; // 不可达
|
|
145
172
|
}
|
|
146
|
-
// 成功路径:响应 body
|
|
173
|
+
// 成功路径:响应 body 通常是原始 CSV/JSON 字节,但部分错误场景下网关会返
|
|
174
|
+
// HTTP 200 + JSON envelope(status_code != "0"),需要在这里嗅探兜底。
|
|
147
175
|
const contentType = response.headers.get("Content-Type") ??
|
|
148
176
|
(opts.format === "csv" ? "text/csv" : "application/json");
|
|
149
177
|
const ab = await response.arrayBuffer();
|
|
@@ -151,10 +179,33 @@ async function exportData(opts) {
|
|
|
151
179
|
if (buf.length === 0) {
|
|
152
180
|
throw new error_1.AppError("INTERNAL_DB_ERROR", "Empty export response body");
|
|
153
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;
|
|
154
204
|
return {
|
|
155
205
|
tableName: opts.tableName,
|
|
156
206
|
format: opts.format,
|
|
157
207
|
contentType,
|
|
158
208
|
body: buf,
|
|
209
|
+
recordCount,
|
|
159
210
|
};
|
|
160
211
|
}
|
package/dist/api/db/client.js
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SQLSTATE_MAP = void 0;
|
|
4
|
+
exports.traceHttp = traceHttp;
|
|
4
5
|
exports.ensureInnerSuccess = ensureInnerSuccess;
|
|
5
6
|
exports.extractData = extractData;
|
|
6
7
|
exports.buildInnerUrl = buildInnerUrl;
|
|
7
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
|
+
}
|
|
8
43
|
/**
|
|
9
44
|
* 校验 dataloom InnerAPI 响应的 envelope。
|
|
10
45
|
*
|
|
@@ -19,21 +54,27 @@ function ensureInnerSuccess(body) {
|
|
|
19
54
|
const code = body.status_code ?? body.ErrorCode ?? "0";
|
|
20
55
|
if (code === "0" || code === "")
|
|
21
56
|
return;
|
|
22
|
-
const message = body.error_msg ?? body.ErrorMessage ?? `dataloom API error [${code}]
|
|
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;
|
|
23
62
|
// k_dl_1300002 是 PG 执行透传错误;error_msg 里常带 SQLSTATE,优先按 SQLSTATE 映射
|
|
24
63
|
if (code === "k_dl_1300002") {
|
|
25
64
|
const sqlstate = extractSqlstate(message);
|
|
26
65
|
if (sqlstate && exports.SQLSTATE_MAP[sqlstate]) {
|
|
27
|
-
throw new error_1.AppError(exports.SQLSTATE_MAP[sqlstate], message);
|
|
66
|
+
throw new error_1.AppError(exports.SQLSTATE_MAP[sqlstate], message, { statement_index: stmtIdx });
|
|
28
67
|
}
|
|
29
68
|
}
|
|
30
69
|
// k_dl_1600000 是 dataloom 通用参数错误,不能整体映射;
|
|
31
|
-
//
|
|
32
|
-
//
|
|
70
|
+
// message 以 "Invalid DB Branch" 开头的两种触发场景:
|
|
71
|
+
// 1) 单环境应用使用 --env(多环境未初始化)
|
|
72
|
+
// 2) 多环境应用传入了不存在的 env 名(如 --env staging)
|
|
73
|
+
// 两者外部表象一致:dbBranch 在 db_branch 表里查不到,固定映射为 MULTI_ENV_NOT_INITIALIZED。
|
|
33
74
|
if (code === "k_dl_1600000" && message.startsWith("Invalid DB Branch")) {
|
|
34
75
|
throw new error_1.AppError("MULTI_ENV_NOT_INITIALIZED", "--env is not available (multi-env not initialized)", {
|
|
35
76
|
next_actions: [
|
|
36
|
-
"
|
|
77
|
+
"Verify the --env value matches an existing dbBranch.",
|
|
37
78
|
],
|
|
38
79
|
});
|
|
39
80
|
}
|
|
@@ -42,10 +83,19 @@ function ensureInnerSuccess(body) {
|
|
|
42
83
|
if (mapped) {
|
|
43
84
|
throw new error_1.AppError(mapped.code, mapped.message ?? message, {
|
|
44
85
|
next_actions: mapped.hint ? [mapped.hint] : undefined,
|
|
86
|
+
statement_index: stmtIdx,
|
|
45
87
|
});
|
|
46
88
|
}
|
|
47
89
|
// 兜底:dataloom 未映射的 code 原样透传
|
|
48
|
-
throw new error_1.AppError(`DB_API_${code}`, message);
|
|
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*/, "");
|
|
49
99
|
}
|
|
50
100
|
/** 从 PG 执行错误消息里提取 "(SQLSTATE XXXXX)"。 */
|
|
51
101
|
function extractSqlstate(msg) {
|
|
@@ -89,11 +139,19 @@ function extractData(body) {
|
|
|
89
139
|
return body.data;
|
|
90
140
|
}
|
|
91
141
|
/**
|
|
92
|
-
* 组装 `/v1/app/{appId}
|
|
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。
|
|
93
151
|
*/
|
|
94
152
|
function buildInnerUrl(appId, path, query) {
|
|
95
153
|
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
96
|
-
let url = `/v1/app/${encodeURIComponent(appId)}
|
|
154
|
+
let url = `/v1/dataloom/app/${encodeURIComponent(appId)}${normalized}`;
|
|
97
155
|
if (query) {
|
|
98
156
|
const usp = new URLSearchParams();
|
|
99
157
|
for (const [k, v] of Object.entries(query)) {
|
package/dist/api/db/parsers.js
CHANGED
|
@@ -27,16 +27,23 @@ function parseSqlResult(r) {
|
|
|
27
27
|
recordCount: r.recordCount ?? rows.length,
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
-
if (r.sqlType === "INSERT" ||
|
|
30
|
+
if (r.sqlType === "INSERT" ||
|
|
31
|
+
r.sqlType === "UPDATE" ||
|
|
32
|
+
r.sqlType === "DELETE" ||
|
|
33
|
+
r.sqlType === "MERGE" ||
|
|
34
|
+
r.sqlType === "DML") {
|
|
31
35
|
const affected = r.affectedRows ?? extractRowCount(r.data);
|
|
32
36
|
return {
|
|
33
37
|
kind: "dml",
|
|
38
|
+
// 上面已 narrow,这里 cast 是为了 SqlType 联合里的 (string & {}) 让 TS 无法
|
|
39
|
+
// 自动收窄到字面量集合,不影响运行时安全
|
|
34
40
|
sqlType: r.sqlType,
|
|
35
41
|
affectedRows: affected,
|
|
36
42
|
};
|
|
37
43
|
}
|
|
38
|
-
// DDL or unknown
|
|
39
|
-
|
|
44
|
+
// DDL or unknown — sqlType 透传后端给的细粒度(CREATE_TABLE / DROP_TABLE / ...
|
|
45
|
+
// / 笼统 "DDL"),CLI JSON 输出直接当 command 用
|
|
46
|
+
return { kind: "ddl", sqlType: r.sqlType };
|
|
40
47
|
}
|
|
41
48
|
/** DML 的 data 通常是 `[{"rowCount": N}]`;兜底从这里读影响行数。 */
|
|
42
49
|
function extractRowCount(data) {
|
|
@@ -82,7 +89,6 @@ function toSummary(t, stats) {
|
|
|
82
89
|
columns: (t.fields ?? []).length,
|
|
83
90
|
estimated_row_count: typeof stats?.estimatedRowCount === "number" ? stats.estimatedRowCount : null,
|
|
84
91
|
size_bytes: typeof stats?.sizeBytes === "number" ? stats.sizeBytes : null,
|
|
85
|
-
updated_at: t.updatedAt,
|
|
86
92
|
};
|
|
87
93
|
}
|
|
88
94
|
/**
|
|
@@ -118,8 +124,6 @@ function toDetail(t, stats) {
|
|
|
118
124
|
indexes: rawIndexes.map(toIndex),
|
|
119
125
|
estimated_row_count: typeof stats?.estimatedRowCount === "number" ? stats.estimatedRowCount : null,
|
|
120
126
|
size_bytes: typeof stats?.sizeBytes === "number" ? stats.sizeBytes : null,
|
|
121
|
-
created_at: t.createdAt,
|
|
122
|
-
updated_at: t.updatedAt,
|
|
123
127
|
};
|
|
124
128
|
}
|
|
125
129
|
function toColumn(f) {
|
package/dist/api/file/api.js
CHANGED
|
@@ -36,7 +36,7 @@ function extractEnvelope(body) {
|
|
|
36
36
|
// ── list ──
|
|
37
37
|
/** 底层单次 list 调用(一页)。支持 filterExpr / sortBy 下推。 */
|
|
38
38
|
async function listOnce(appId, bucketId, opts) {
|
|
39
|
-
const url = `/
|
|
39
|
+
const url = `/v1/storage/app/${encodeURIComponent(appId)}/bucket/${encodeURIComponent(bucketId)}/list`;
|
|
40
40
|
const reqBody = {
|
|
41
41
|
bucketID: bucketId,
|
|
42
42
|
maxKeys: opts.limit,
|
|
@@ -94,7 +94,7 @@ function buildFilterExpr(opts) {
|
|
|
94
94
|
/**
|
|
95
95
|
* 列出文件:所有过滤下推到后端 FilterExpression,纯精确匹配,无 glob。
|
|
96
96
|
*
|
|
97
|
-
* 后端:POST /
|
|
97
|
+
* 后端:POST /v1/storage/app/{appId}/bucket/{bucketId}/list
|
|
98
98
|
*/
|
|
99
99
|
async function listFiles(opts) {
|
|
100
100
|
const bucketId = await (0, client_1.getDefaultBucketId)(opts.appId);
|
|
@@ -214,7 +214,7 @@ async function resolveByName(appId, input) {
|
|
|
214
214
|
error: {
|
|
215
215
|
code: "FILE_NOT_FOUND",
|
|
216
216
|
message: `File '${input}' does not exist`,
|
|
217
|
-
hint: "Run `miaoda file ls` to
|
|
217
|
+
hint: "Run `miaoda file ls` to see available files.",
|
|
218
218
|
},
|
|
219
219
|
};
|
|
220
220
|
}
|
|
@@ -225,7 +225,7 @@ async function resolveByName(appId, input) {
|
|
|
225
225
|
error: {
|
|
226
226
|
code: "AMBIGUOUS_FILE_NAME",
|
|
227
227
|
message: `Multiple files match name '${input}' (${String(matches.length)} found)`,
|
|
228
|
-
hint: `Use
|
|
228
|
+
hint: `Use path instead. Run \`miaoda file ls --name ${input}\` to see candidates.`,
|
|
229
229
|
},
|
|
230
230
|
};
|
|
231
231
|
}
|
|
@@ -234,12 +234,12 @@ async function resolveByName(appId, input) {
|
|
|
234
234
|
// ── stat ──
|
|
235
235
|
/**
|
|
236
236
|
* 查看文件元数据。
|
|
237
|
-
* 后端:GET /
|
|
237
|
+
* 后端:GET /v1/storage/app/{appId}/object/{bucketId}/{filePath}/head
|
|
238
238
|
*/
|
|
239
239
|
async function statFile(opts) {
|
|
240
240
|
const bucketId = await (0, client_1.getDefaultBucketId)(opts.appId);
|
|
241
241
|
// 对齐 file-storage-skill Py 版:URL 顺序是 .../object/{bucket}/head/{path}
|
|
242
|
-
const url = `/
|
|
242
|
+
const url = `/v1/storage/app/${encodeURIComponent(opts.appId)}/object/${encodeURIComponent(bucketId)}/head/${encodePath(opts.filePath)}`;
|
|
243
243
|
const body = await (0, client_1.doGet)(url, {
|
|
244
244
|
notFoundCode: "FILE_NOT_FOUND",
|
|
245
245
|
notFoundMessage: `File '${opts.filePath}' does not exist`,
|
|
@@ -251,12 +251,12 @@ async function statFile(opts) {
|
|
|
251
251
|
// ── 预签上传 URL ──
|
|
252
252
|
async function preUpload(appId, req) {
|
|
253
253
|
const bucketId = await (0, client_1.getDefaultBucketId)(appId);
|
|
254
|
-
const url = `/
|
|
254
|
+
const url = `/v1/storage/app/${encodeURIComponent(appId)}/bucket/${encodeURIComponent(bucketId)}/preUpload`;
|
|
255
255
|
const body = await (0, client_1.doPost)(url, { bucketID: bucketId, appID: appId, ...req }, { errorContext: "pre-upload" });
|
|
256
256
|
return extractEnvelope(body);
|
|
257
257
|
}
|
|
258
258
|
async function uploadCallback(appId, req) {
|
|
259
|
-
const url = `/
|
|
259
|
+
const url = `/v1/storage/app/${encodeURIComponent(appId)}/object/callback`;
|
|
260
260
|
const body = await (0, client_1.doPost)(url, req, { errorContext: "upload callback" });
|
|
261
261
|
(0, client_1.ensureSuccess)(body);
|
|
262
262
|
}
|
|
@@ -280,6 +280,7 @@ async function uploadFile(opts) {
|
|
|
280
280
|
// Node 20+ 内置 fetch 运行时接受 Buffer,但 TS 的 BodyInit 要求更严格的类型;
|
|
281
281
|
// 这里复制到独立 ArrayBuffer 以满足 lib.dom.d.ts 的 BodyInit 约束
|
|
282
282
|
const ab = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
|
|
283
|
+
const uploadStart = Date.now();
|
|
283
284
|
const res = await fetch(pre.uploadURL, {
|
|
284
285
|
method: "PUT",
|
|
285
286
|
headers: {
|
|
@@ -288,6 +289,7 @@ async function uploadFile(opts) {
|
|
|
288
289
|
},
|
|
289
290
|
body: ab,
|
|
290
291
|
});
|
|
292
|
+
(0, logger_1.debug)(`http PUT <upload-cdn> ${String(res.status)} cost=${String(Date.now() - uploadStart)}ms size=${String(opts.fileSize)}`);
|
|
291
293
|
if (!res.ok) {
|
|
292
294
|
throw new error_1.AppError("FILE_UPLOAD_FAILED", `Upload PUT failed with status ${String(res.status)}`);
|
|
293
295
|
}
|
|
@@ -332,11 +334,11 @@ async function uploadFile(opts) {
|
|
|
332
334
|
// ── 预签下载 URL ──
|
|
333
335
|
/**
|
|
334
336
|
* 获取预签下载 URL。
|
|
335
|
-
* 后端:POST /
|
|
337
|
+
* 后端:POST /v1/storage/app/{appId}/object/sign/{bucketId}/{filePath}
|
|
336
338
|
*/
|
|
337
339
|
async function signDownload(opts) {
|
|
338
340
|
const bucketId = await (0, client_1.getDefaultBucketId)(opts.appId);
|
|
339
|
-
const url = `/
|
|
341
|
+
const url = `/v1/storage/app/${encodeURIComponent(opts.appId)}/object/sign/${encodeURIComponent(bucketId)}/${encodePath(opts.filePath)}`;
|
|
340
342
|
const reqBody = {
|
|
341
343
|
bucketID: bucketId,
|
|
342
344
|
filePath: toApiPath(opts.filePath),
|
|
@@ -364,12 +366,15 @@ async function signDownload(opts) {
|
|
|
364
366
|
*/
|
|
365
367
|
async function downloadFile(opts) {
|
|
366
368
|
let res;
|
|
369
|
+
const downloadStart = Date.now();
|
|
367
370
|
try {
|
|
368
371
|
res = await fetch(opts.signedURL);
|
|
369
372
|
}
|
|
370
373
|
catch (err) {
|
|
374
|
+
(0, logger_1.debug)(`http GET <download-cdn> 0 cost=${String(Date.now() - downloadStart)}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
371
375
|
throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download GET failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
372
376
|
}
|
|
377
|
+
(0, logger_1.debug)(`http GET <download-cdn> ${String(res.status)} cost=${String(Date.now() - downloadStart)}ms`);
|
|
373
378
|
if (!res.ok) {
|
|
374
379
|
throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download failed with status ${String(res.status)}`);
|
|
375
380
|
}
|
|
@@ -381,11 +386,11 @@ async function downloadFile(opts) {
|
|
|
381
386
|
// ── 批量删除(best-effort) ──
|
|
382
387
|
/**
|
|
383
388
|
* 批量删除文件。
|
|
384
|
-
* 后端:DELETE /
|
|
389
|
+
* 后端:DELETE /v1/storage/app/{appId}/bucket/{bucketId}/delete
|
|
385
390
|
*/
|
|
386
391
|
async function deleteFiles(opts) {
|
|
387
392
|
const bucketId = await (0, client_1.getDefaultBucketId)(opts.appId);
|
|
388
|
-
const url = `/
|
|
393
|
+
const url = `/v1/storage/app/${encodeURIComponent(opts.appId)}/bucket/${encodeURIComponent(bucketId)}/delete`;
|
|
389
394
|
const reqBody = {
|
|
390
395
|
bucketID: bucketId,
|
|
391
396
|
filePaths: opts.filePaths.map(toApiPath),
|