@lark-apaas/miaoda-cli 0.1.0-alpha.ca2912e → 0.1.0-alpha.d98ea14
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 +119 -70
- package/dist/api/db/client.js +43 -3
- package/dist/api/db/parsers.js +10 -6
- package/dist/api/file/api.js +53 -6
- package/dist/api/file/client.js +36 -0
- package/dist/api/file/index.js +2 -1
- package/dist/cli/commands/db/index.js +149 -32
- package/dist/cli/commands/file/index.js +148 -36
- package/dist/cli/commands/plugin/index.js +2 -1
- package/dist/cli/commands/shared.js +4 -10
- package/dist/cli/handlers/db/data.js +5 -4
- package/dist/cli/handlers/db/schema.js +6 -6
- package/dist/cli/handlers/db/sql.js +207 -19
- package/dist/cli/handlers/file/ls.js +3 -1
- package/dist/cli/handlers/file/rm.js +27 -7
- 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 +1 -1
- package/dist/utils/output.js +3 -0
- package/package.json +2 -2
package/dist/api/db/api.js
CHANGED
|
@@ -6,7 +6,36 @@ 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");
|
|
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
|
+
}
|
|
10
39
|
// CLI 不再为 dbBranch 设默认值:
|
|
11
40
|
// 用户没传 --env 就完全不携带 dbBranch query 参数,由后端 admin-inner 中间件
|
|
12
41
|
// 按 workspace 多环境状态决定(多环境 → dev / 单环境 → main)。
|
|
@@ -24,19 +53,16 @@ async function execSql(opts) {
|
|
|
24
53
|
const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/sql", {
|
|
25
54
|
dbBranch: opts.dbBranch,
|
|
26
55
|
});
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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}`);
|
|
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; // 不可达
|
|
40
66
|
}
|
|
41
67
|
const body = (await response.json());
|
|
42
68
|
const data = (0, client_1.extractData)(body);
|
|
@@ -57,95 +83,95 @@ async function getSchema(opts) {
|
|
|
57
83
|
includeStats: opts.includeStats ? "true" : undefined,
|
|
58
84
|
dbBranch: opts.dbBranch,
|
|
59
85
|
});
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
(0, client_1.extractData)(body);
|
|
71
|
-
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; // 不可达
|
|
72
96
|
}
|
|
73
97
|
const body = (await response.json());
|
|
74
98
|
return (0, client_1.extractData)(body);
|
|
75
99
|
}
|
|
76
|
-
// ── db data import →
|
|
100
|
+
// ── db data import → InnerAdminImportData ──
|
|
77
101
|
/**
|
|
78
102
|
* 导入文件。
|
|
79
|
-
* 后端:POST /v1/dataloom/app/{appId}/data/import
|
|
103
|
+
* 后端:POST /v1/dataloom/app/{appId}/data/import
|
|
104
|
+
*
|
|
105
|
+
* 全字段走 JSON body envelope(idl-larkgw 36125a2f):
|
|
106
|
+
* {tableName, format, records, dbBranch?}
|
|
80
107
|
*
|
|
81
|
-
*
|
|
108
|
+
* `records` 字段携带 CSV / JSON 文本内容(utf8 字符串),与 dataloom 上
|
|
109
|
+
* Import/ExportAdminRecords 命名风格对齐。CLI 端把 Buffer 解码成 utf8
|
|
110
|
+
* 字符串后塞进 envelope 即可。
|
|
82
111
|
*/
|
|
83
112
|
async function importData(opts) {
|
|
84
113
|
const client = (0, http_1.getHttpClient)();
|
|
85
|
-
const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import"
|
|
114
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import");
|
|
115
|
+
const reqBody = {
|
|
86
116
|
tableName: opts.tableName,
|
|
87
117
|
format: opts.format,
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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}`);
|
|
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; // 不可达
|
|
109
133
|
}
|
|
110
|
-
// 后端
|
|
134
|
+
// 后端 InnerAdminImportData 响应里 data 直接返 {tableName, recordCount, durationMs}
|
|
111
135
|
const body = (await response.json());
|
|
112
136
|
const data = (0, client_1.extractData)(body);
|
|
113
137
|
return {
|
|
114
138
|
tableName: data.tableName ?? opts.tableName,
|
|
115
|
-
|
|
139
|
+
recordCount: data.recordCount ?? 0,
|
|
116
140
|
durationMs: data.durationMs ?? 0,
|
|
117
141
|
};
|
|
118
142
|
}
|
|
119
|
-
// ── db data export →
|
|
143
|
+
// ── db data export → InnerAdminExportData ──
|
|
120
144
|
/**
|
|
121
145
|
* 导出数据。
|
|
122
|
-
* 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&dbBranch=main
|
|
146
|
+
* 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&limit=5000&dbBranch=main
|
|
123
147
|
*
|
|
124
|
-
*
|
|
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。
|
|
125
152
|
*/
|
|
126
153
|
async function exportData(opts) {
|
|
127
154
|
const client = (0, http_1.getHttpClient)();
|
|
128
155
|
const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/export", {
|
|
129
156
|
tableName: opts.tableName,
|
|
130
157
|
format: opts.format,
|
|
158
|
+
limit: String(opts.limit ?? 5000),
|
|
131
159
|
dbBranch: opts.dbBranch,
|
|
132
160
|
});
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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}`);
|
|
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; // 不可达
|
|
147
172
|
}
|
|
148
|
-
// 成功路径:响应 body
|
|
173
|
+
// 成功路径:响应 body 通常是原始 CSV/JSON 字节,但部分错误场景下网关会返
|
|
174
|
+
// HTTP 200 + JSON envelope(status_code != "0"),需要在这里嗅探兜底。
|
|
149
175
|
const contentType = response.headers.get("Content-Type") ??
|
|
150
176
|
(opts.format === "csv" ? "text/csv" : "application/json");
|
|
151
177
|
const ab = await response.arrayBuffer();
|
|
@@ -153,10 +179,33 @@ async function exportData(opts) {
|
|
|
153
179
|
if (buf.length === 0) {
|
|
154
180
|
throw new error_1.AppError("INTERNAL_DB_ERROR", "Empty export response body");
|
|
155
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;
|
|
156
204
|
return {
|
|
157
205
|
tableName: opts.tableName,
|
|
158
206
|
format: opts.format,
|
|
159
207
|
contentType,
|
|
160
208
|
body: buf,
|
|
209
|
+
recordCount,
|
|
161
210
|
};
|
|
162
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
|
*
|
|
@@ -20,11 +55,15 @@ function ensureInnerSuccess(body) {
|
|
|
20
55
|
if (code === "0" || code === "")
|
|
21
56
|
return;
|
|
22
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 通用参数错误,不能整体映射;
|
|
@@ -35,7 +74,7 @@ function ensureInnerSuccess(body) {
|
|
|
35
74
|
if (code === "k_dl_1600000" && message.startsWith("Invalid DB Branch")) {
|
|
36
75
|
throw new error_1.AppError("MULTI_ENV_NOT_INITIALIZED", "--env is not available (multi-env not initialized)", {
|
|
37
76
|
next_actions: [
|
|
38
|
-
"Verify the --env value matches an existing dbBranch
|
|
77
|
+
"Verify the --env value matches an existing dbBranch.",
|
|
39
78
|
],
|
|
40
79
|
});
|
|
41
80
|
}
|
|
@@ -44,10 +83,11 @@ function ensureInnerSuccess(body) {
|
|
|
44
83
|
if (mapped) {
|
|
45
84
|
throw new error_1.AppError(mapped.code, mapped.message ?? message, {
|
|
46
85
|
next_actions: mapped.hint ? [mapped.hint] : undefined,
|
|
86
|
+
statement_index: stmtIdx,
|
|
47
87
|
});
|
|
48
88
|
}
|
|
49
89
|
// 兜底:dataloom 未映射的 code 原样透传
|
|
50
|
-
throw new error_1.AppError(`DB_API_${code}`, message);
|
|
90
|
+
throw new error_1.AppError(`DB_API_${code}`, message, { statement_index: stmtIdx });
|
|
51
91
|
}
|
|
52
92
|
/**
|
|
53
93
|
* 剥掉 PG 透传错误的 "ERROR:" 前缀:CLI 输出本身就会前缀 "Error:",
|
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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseTimeFilterMs = parseTimeFilterMs;
|
|
3
4
|
exports.listFiles = listFiles;
|
|
4
5
|
exports.resolveInputs = resolveInputs;
|
|
5
6
|
exports.statFile = statFile;
|
|
@@ -82,15 +83,56 @@ function buildFilterExpr(opts) {
|
|
|
82
83
|
}
|
|
83
84
|
if (opts.uploadedSince) {
|
|
84
85
|
// 后端期望毫秒 timestamp(strconv.ParseInt → time.UnixMilli)
|
|
85
|
-
const ms =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
const ms = parseTimeFilterMs(opts.uploadedSince, "--uploaded-since");
|
|
87
|
+
conds.push({ field: "createdAt", operator: "gte", value: String(ms) });
|
|
88
|
+
}
|
|
89
|
+
if (opts.uploadedUntil) {
|
|
90
|
+
const ms = parseTimeFilterMs(opts.uploadedUntil, "--uploaded-until");
|
|
91
|
+
conds.push({ field: "createdAt", operator: "lte", value: String(ms) });
|
|
89
92
|
}
|
|
90
93
|
if (conds.length === 0)
|
|
91
94
|
return undefined;
|
|
92
95
|
return { logic: "and", groups: [{ conditions: conds }] };
|
|
93
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* 解析时间过滤参数(--uploaded-since / --uploaded-until)的三种输入格式
|
|
99
|
+
* (→ ms timestamp):
|
|
100
|
+
* 1. 相对时间:"30s" / "10m" / "1h" / "2d" / "1w" → 当前时间往前推 N 单位
|
|
101
|
+
* 2. 日期: "2026-04-01" → 按当日 00:00:00 UTC
|
|
102
|
+
* 3. ISO 8601:"2026-04-01T10:00:00Z" → 严格解析(推荐用 Z 结尾标识 UTC)
|
|
103
|
+
*
|
|
104
|
+
* 三种格式都不命中时抛 AppError("ARGS_INVALID"),避免之前 Date.parse=NaN
|
|
105
|
+
* 时静默跳过过滤、用户却以为筛选生效的悄然失败问题。
|
|
106
|
+
*
|
|
107
|
+
* flagName 用于错误信息,调用方传 "--uploaded-since" 或 "--uploaded-until"。
|
|
108
|
+
*/
|
|
109
|
+
function parseTimeFilterMs(input, flagName) {
|
|
110
|
+
// 相对时间:<positive int><unit>,单位 s/m/h/d/w
|
|
111
|
+
const RELATIVE = /^(\d+)([smhdw])$/;
|
|
112
|
+
const rel = RELATIVE.exec(input);
|
|
113
|
+
if (rel) {
|
|
114
|
+
const n = parseInt(rel[1], 10);
|
|
115
|
+
if (n <= 0) {
|
|
116
|
+
throw new error_1.AppError("ARGS_INVALID", `${flagName} "${input}" 必须是正整数 + 单位(s / m / h / d / w)`);
|
|
117
|
+
}
|
|
118
|
+
const UNIT_MS = {
|
|
119
|
+
s: 1_000,
|
|
120
|
+
m: 60_000,
|
|
121
|
+
h: 3_600_000,
|
|
122
|
+
d: 86_400_000,
|
|
123
|
+
w: 604_800_000,
|
|
124
|
+
};
|
|
125
|
+
return Date.now() - n * UNIT_MS[rel[2]];
|
|
126
|
+
}
|
|
127
|
+
// 绝对时间:date / ISO 8601。Date.parse 对 "YYYY-MM-DD" 按 UTC 00:00:00 解析,
|
|
128
|
+
// 对带 Z 的 ISO 8601 也直接出 UTC ms。
|
|
129
|
+
const ms = Date.parse(input);
|
|
130
|
+
if (Number.isNaN(ms)) {
|
|
131
|
+
throw new error_1.AppError("ARGS_INVALID", `${flagName} "${input}" 格式无法识别。支持:` +
|
|
132
|
+
`相对时间(如 1h / 2d / 1w)、日期(YYYY-MM-DD)、ISO 8601(如 2026-04-01T10:00:00Z)`);
|
|
133
|
+
}
|
|
134
|
+
return ms;
|
|
135
|
+
}
|
|
94
136
|
/**
|
|
95
137
|
* 列出文件:所有过滤下推到后端 FilterExpression,纯精确匹配,无 glob。
|
|
96
138
|
*
|
|
@@ -214,7 +256,7 @@ async function resolveByName(appId, input) {
|
|
|
214
256
|
error: {
|
|
215
257
|
code: "FILE_NOT_FOUND",
|
|
216
258
|
message: `File '${input}' does not exist`,
|
|
217
|
-
hint: "Run `miaoda file ls` to
|
|
259
|
+
hint: "Run `miaoda file ls` to see available files.",
|
|
218
260
|
},
|
|
219
261
|
};
|
|
220
262
|
}
|
|
@@ -225,7 +267,7 @@ async function resolveByName(appId, input) {
|
|
|
225
267
|
error: {
|
|
226
268
|
code: "AMBIGUOUS_FILE_NAME",
|
|
227
269
|
message: `Multiple files match name '${input}' (${String(matches.length)} found)`,
|
|
228
|
-
hint: `Use
|
|
270
|
+
hint: `Use path instead. Run \`miaoda file ls --name ${input}\` to see candidates.`,
|
|
229
271
|
},
|
|
230
272
|
};
|
|
231
273
|
}
|
|
@@ -280,6 +322,7 @@ async function uploadFile(opts) {
|
|
|
280
322
|
// Node 20+ 内置 fetch 运行时接受 Buffer,但 TS 的 BodyInit 要求更严格的类型;
|
|
281
323
|
// 这里复制到独立 ArrayBuffer 以满足 lib.dom.d.ts 的 BodyInit 约束
|
|
282
324
|
const ab = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
|
|
325
|
+
const uploadStart = Date.now();
|
|
283
326
|
const res = await fetch(pre.uploadURL, {
|
|
284
327
|
method: "PUT",
|
|
285
328
|
headers: {
|
|
@@ -288,6 +331,7 @@ async function uploadFile(opts) {
|
|
|
288
331
|
},
|
|
289
332
|
body: ab,
|
|
290
333
|
});
|
|
334
|
+
(0, logger_1.debug)(`http PUT <upload-cdn> ${String(res.status)} cost=${String(Date.now() - uploadStart)}ms size=${String(opts.fileSize)}`);
|
|
291
335
|
if (!res.ok) {
|
|
292
336
|
throw new error_1.AppError("FILE_UPLOAD_FAILED", `Upload PUT failed with status ${String(res.status)}`);
|
|
293
337
|
}
|
|
@@ -364,12 +408,15 @@ async function signDownload(opts) {
|
|
|
364
408
|
*/
|
|
365
409
|
async function downloadFile(opts) {
|
|
366
410
|
let res;
|
|
411
|
+
const downloadStart = Date.now();
|
|
367
412
|
try {
|
|
368
413
|
res = await fetch(opts.signedURL);
|
|
369
414
|
}
|
|
370
415
|
catch (err) {
|
|
416
|
+
(0, logger_1.debug)(`http GET <download-cdn> 0 cost=${String(Date.now() - downloadStart)}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
371
417
|
throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download GET failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
372
418
|
}
|
|
419
|
+
(0, logger_1.debug)(`http GET <download-cdn> ${String(res.status)} cost=${String(Date.now() - downloadStart)}ms`);
|
|
373
420
|
if (!res.ok) {
|
|
374
421
|
throw new error_1.AppError("FILE_DOWNLOAD_FAILED", `Download failed with status ${String(res.status)}`);
|
|
375
422
|
}
|
package/dist/api/file/client.js
CHANGED
|
@@ -8,7 +8,34 @@ exports.doPost = doPost;
|
|
|
8
8
|
exports.doRequest = doRequest;
|
|
9
9
|
const http_1 = require("../../utils/http");
|
|
10
10
|
const error_1 = require("../../utils/error");
|
|
11
|
+
const logger_1 = require("../../utils/logger");
|
|
11
12
|
const http_client_1 = require("@lark-apaas/http-client");
|
|
13
|
+
/**
|
|
14
|
+
* 输出一条 HTTP 调试日志(仅 --verbose 模式生效)。
|
|
15
|
+
*
|
|
16
|
+
* 主要用于把后端返回的 `x-tt-logid` 透出给用户,方便拿这个 id 去 server / 网关日志里
|
|
17
|
+
* 直接定位本次请求的 `[MiaodaCLI.metric]` 行与上下游 trace。
|
|
18
|
+
*/
|
|
19
|
+
function traceHttp(method, url, start, response, err) {
|
|
20
|
+
try {
|
|
21
|
+
const cost = Date.now() - start;
|
|
22
|
+
const status = response?.status ?? 0;
|
|
23
|
+
const logid = response?.headers?.get?.("x-tt-logid") ?? "-";
|
|
24
|
+
if (err !== undefined) {
|
|
25
|
+
const errMsg = err instanceof Error
|
|
26
|
+
? err.message
|
|
27
|
+
: typeof err === "string"
|
|
28
|
+
? err
|
|
29
|
+
: JSON.stringify(err);
|
|
30
|
+
(0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid} err=${errMsg}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
(0, logger_1.debug)(`http ${method} ${url} ${String(status)} cost=${String(cost)}ms x-tt-logid=${logid}`);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// debug 失败不应影响业务,吞掉
|
|
37
|
+
}
|
|
38
|
+
}
|
|
12
39
|
/** 进程内 bucket 缓存:{appId: bucketId}。不跨进程。 */
|
|
13
40
|
const bucketCache = new Map();
|
|
14
41
|
/**
|
|
@@ -130,33 +157,42 @@ async function mapHttpError(err, opts) {
|
|
|
130
157
|
* 通过第三个参数显式传入 getRuntimeHttpClient() 切换。
|
|
131
158
|
*/
|
|
132
159
|
async function doGet(url, opts = {}, client = (0, http_1.getHttpClient)()) {
|
|
160
|
+
const start = Date.now();
|
|
133
161
|
try {
|
|
134
162
|
const response = await client.get(url);
|
|
163
|
+
traceHttp("GET", url, start, response);
|
|
135
164
|
return (await response.json());
|
|
136
165
|
}
|
|
137
166
|
catch (err) {
|
|
167
|
+
traceHttp("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
|
|
138
168
|
await mapHttpError(err, opts);
|
|
139
169
|
throw err; // 不可达,mapHttpError 必定 throw
|
|
140
170
|
}
|
|
141
171
|
}
|
|
142
172
|
/** 包一层 client.post + HttpError 统一映射。默认 admin innerapi,可显式切 runtime。 */
|
|
143
173
|
async function doPost(url, body, opts = {}, client = (0, http_1.getHttpClient)()) {
|
|
174
|
+
const start = Date.now();
|
|
144
175
|
try {
|
|
145
176
|
const response = await client.post(url, body);
|
|
177
|
+
traceHttp("POST", url, start, response);
|
|
146
178
|
return (await response.json());
|
|
147
179
|
}
|
|
148
180
|
catch (err) {
|
|
181
|
+
traceHttp("POST", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
|
|
149
182
|
await mapHttpError(err, opts);
|
|
150
183
|
throw err;
|
|
151
184
|
}
|
|
152
185
|
}
|
|
153
186
|
/** 包一层 client.request + HttpError 统一映射(DELETE 带 body 的场景)。 */
|
|
154
187
|
async function doRequest(cfg, opts = {}, client = (0, http_1.getHttpClient)()) {
|
|
188
|
+
const start = Date.now();
|
|
155
189
|
try {
|
|
156
190
|
const response = await client.request(cfg);
|
|
191
|
+
traceHttp(cfg.method, cfg.url, start, response);
|
|
157
192
|
return (await response.json());
|
|
158
193
|
}
|
|
159
194
|
catch (err) {
|
|
195
|
+
traceHttp(cfg.method, cfg.url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
|
|
160
196
|
await mapHttpError(err, opts);
|
|
161
197
|
throw err;
|
|
162
198
|
}
|
package/dist/api/file/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.toAbsolutePath = exports.looksLikePath = exports.resetBucketCache = exports.getDefaultBucketId = exports.resolveInputs = exports.deleteFiles = exports.downloadFile = exports.signDownload = exports.uploadFile = exports.statFile = exports.listFiles = void 0;
|
|
3
|
+
exports.toAbsolutePath = exports.looksLikePath = exports.resetBucketCache = exports.getDefaultBucketId = exports.parseTimeFilterMs = exports.resolveInputs = exports.deleteFiles = exports.downloadFile = exports.signDownload = exports.uploadFile = exports.statFile = exports.listFiles = void 0;
|
|
4
4
|
var api_1 = require("./api");
|
|
5
5
|
Object.defineProperty(exports, "listFiles", { enumerable: true, get: function () { return api_1.listFiles; } });
|
|
6
6
|
Object.defineProperty(exports, "statFile", { enumerable: true, get: function () { return api_1.statFile; } });
|
|
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "signDownload", { enumerable: true, get: function
|
|
|
9
9
|
Object.defineProperty(exports, "downloadFile", { enumerable: true, get: function () { return api_1.downloadFile; } });
|
|
10
10
|
Object.defineProperty(exports, "deleteFiles", { enumerable: true, get: function () { return api_1.deleteFiles; } });
|
|
11
11
|
Object.defineProperty(exports, "resolveInputs", { enumerable: true, get: function () { return api_1.resolveInputs; } });
|
|
12
|
+
Object.defineProperty(exports, "parseTimeFilterMs", { enumerable: true, get: function () { return api_1.parseTimeFilterMs; } });
|
|
12
13
|
var client_1 = require("./client");
|
|
13
14
|
Object.defineProperty(exports, "getDefaultBucketId", { enumerable: true, get: function () { return client_1.getDefaultBucketId; } });
|
|
14
15
|
Object.defineProperty(exports, "resetBucketCache", { enumerable: true, get: function () { return client_1.resetBucketCache; } });
|