@lark-apaas/miaoda-cli 0.1.0-alpha.08508f4

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.
Files changed (47) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +66 -0
  3. package/bin/miaoda.js +2 -0
  4. package/dist/api/db/api.js +200 -0
  5. package/dist/api/db/client.js +166 -0
  6. package/dist/api/db/index.js +16 -0
  7. package/dist/api/db/parsers.js +161 -0
  8. package/dist/api/db/types.js +10 -0
  9. package/dist/api/file/api.js +425 -0
  10. package/dist/api/file/client.js +199 -0
  11. package/dist/api/file/detect.js +56 -0
  12. package/dist/api/file/index.js +17 -0
  13. package/dist/api/file/parsers.js +72 -0
  14. package/dist/api/file/types.js +3 -0
  15. package/dist/api/index.js +42 -0
  16. package/dist/api/plugin/api.js +243 -0
  17. package/dist/api/plugin/index.js +12 -0
  18. package/dist/api/plugin/types.js +3 -0
  19. package/dist/cli/commands/db/index.js +69 -0
  20. package/dist/cli/commands/file/index.js +75 -0
  21. package/dist/cli/commands/index.js +11 -0
  22. package/dist/cli/commands/plugin/index.js +204 -0
  23. package/dist/cli/commands/shared.js +52 -0
  24. package/dist/cli/handlers/db/data.js +168 -0
  25. package/dist/cli/handlers/db/index.js +11 -0
  26. package/dist/cli/handlers/db/schema.js +163 -0
  27. package/dist/cli/handlers/db/sql.js +252 -0
  28. package/dist/cli/handlers/file/cp.js +220 -0
  29. package/dist/cli/handlers/file/index.js +13 -0
  30. package/dist/cli/handlers/file/ls.js +110 -0
  31. package/dist/cli/handlers/file/rm.js +263 -0
  32. package/dist/cli/handlers/file/sign.js +96 -0
  33. package/dist/cli/handlers/file/stat.js +97 -0
  34. package/dist/cli/handlers/index.js +19 -0
  35. package/dist/cli/handlers/plugin/index.js +10 -0
  36. package/dist/cli/handlers/plugin/plugin-local.js +382 -0
  37. package/dist/cli/handlers/plugin/plugin.js +308 -0
  38. package/dist/main.js +31 -0
  39. package/dist/utils/config.js +31 -0
  40. package/dist/utils/error.js +38 -0
  41. package/dist/utils/http.js +67 -0
  42. package/dist/utils/index.js +27 -0
  43. package/dist/utils/log_id.js +13 -0
  44. package/dist/utils/logger.js +15 -0
  45. package/dist/utils/output.js +72 -0
  46. package/dist/utils/render.js +187 -0
  47. package/package.json +53 -0
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Lark Technologies Pte. Ltd. and/or its affiliates
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted,provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
8
+ IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE
9
+ INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
10
+ EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
11
+ CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
12
+ DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
13
+ ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # miaoda-cli
2
+
3
+ 妙搭开发平台 Code Agent 命令行工具,为 Agent 提供原子能力。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add -g @lark-apaas/miaoda-cli
9
+ ```
10
+
11
+ ## 使用
12
+
13
+ ```bash
14
+ # 设置默认应用(需要应用上下文的命令可用 --app-id 覆盖)
15
+ export MIAODA_APP_ID=app_demo_xxx
16
+
17
+ # 插件管理
18
+ miaoda plugin list-packages
19
+ miaoda plugin install @demo/example-plugin
20
+ ```
21
+
22
+ JSON 结构化输出(Agent 推荐):
23
+
24
+ ```bash
25
+ # 输出全部字段
26
+ miaoda plugin list --json
27
+
28
+ # 可选字段级选择
29
+ miaoda plugin list --json id,name
30
+
31
+ # 或通过 --output 指定格式
32
+ miaoda plugin list --output json
33
+ ```
34
+
35
+ ## 命令树
36
+
37
+ 命令以"域"作为第一级命名空间:
38
+
39
+ | 域 | 用途 |
40
+ |---|---|
41
+ | `miaoda plugin ...` | 插件管理:安装、更新、移除、插件实例查询 |
42
+
43
+ 完整命令通过 `miaoda --help` 或 `miaoda <domain> --help` 查看。
44
+
45
+ ## 全局参数
46
+
47
+ - `--json [fields]`:输出结构化 JSON,可选字段级选择(如 `--json id,name`)。
48
+ - `--output <pretty|json>`:输出格式,默认 `pretty`。
49
+ - `--verbose`:输出调试日志到 stderr。
50
+
51
+ > `--app-id <id>` 非全局参数,需要应用上下文的命令会各自声明,默认读取 `MIAODA_APP_ID` 环境变量。
52
+
53
+ ## 环境变量
54
+
55
+ - `MIAODA_APP_ID`:默认应用 ID,等价于 `--app-id`。
56
+ - `MIAODA_CANARY_HEADER`:设置后所有 HTTP 请求自动带上 `x-tt-env: <值>` 小流量头(例:`export MIAODA_CANARY_HEADER=boe_xxx`)。
57
+
58
+ ## 输出契约
59
+
60
+ - 默认文本输出(pretty),`--json` 或 `--output json` 切换为 JSON。
61
+ - 错误始终写 stderr,业务数据写 stdout。
62
+ - 完整的输出格式、JSON envelope、错误 shape、错误码前缀、退出码分类等见 [Agent 友好 CLI 设计规范](./docs/agent-friendly-cli.md)。
63
+
64
+ ## 贡献
65
+
66
+ 详见 [AGENTS.md](./AGENTS.md)。
package/bin/miaoda.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require("../dist/main.js");
@@ -0,0 +1,200 @@
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 start = Date.now();
28
+ const response = await client.post(url, { sql: opts.sql });
29
+ (0, client_1.traceHttp)("POST", url, start, response);
30
+ if (!response.ok) {
31
+ // 4xx / 5xx:尝试解析 body 取 status_code 映射业务错误
32
+ let body = null;
33
+ try {
34
+ body = (await response.json());
35
+ }
36
+ catch {
37
+ // ignore
38
+ }
39
+ if (body)
40
+ (0, client_1.extractData)(body);
41
+ throw new error_1.HttpError(response.status, url, `Failed to execute SQL: ${String(response.status)} ${response.statusText}`);
42
+ }
43
+ const body = (await response.json());
44
+ const data = (0, client_1.extractData)(body);
45
+ return data.results ?? [];
46
+ }
47
+ // ── db schema → InnerGetSchema ──
48
+ /**
49
+ * 查询 schema(admin-inner)。
50
+ * 后端:GET /v1/dataloom/app/{appId}/db/schema?format=schema|ddl&tableNames=...&includeStats=...&dbBranch=main
51
+ *
52
+ * 返回 body.data(含 `schema` 或 `ddl`)供 handler 使用。
53
+ */
54
+ async function getSchema(opts) {
55
+ const client = (0, http_1.getHttpClient)();
56
+ const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/schema", {
57
+ format: opts.format ?? "schema",
58
+ tableNames: opts.tableNames,
59
+ includeStats: opts.includeStats ? "true" : undefined,
60
+ dbBranch: opts.dbBranch,
61
+ });
62
+ const start = Date.now();
63
+ const response = await client.get(url);
64
+ (0, client_1.traceHttp)("GET", url, start, response);
65
+ if (!response.ok) {
66
+ let body = null;
67
+ try {
68
+ body = (await response.json());
69
+ }
70
+ catch {
71
+ // ignore
72
+ }
73
+ if (body)
74
+ (0, client_1.extractData)(body);
75
+ throw new error_1.HttpError(response.status, url, `Failed to get schema: ${String(response.status)} ${response.statusText}`);
76
+ }
77
+ const body = (await response.json());
78
+ return (0, client_1.extractData)(body);
79
+ }
80
+ // ── db data import → InnerAdminImportData ──
81
+ /**
82
+ * 导入文件。
83
+ * 后端:POST /v1/dataloom/app/{appId}/data/import
84
+ *
85
+ * 全字段走 JSON body envelope(idl-larkgw 36125a2f):
86
+ * {tableName, format, records, dbBranch?}
87
+ *
88
+ * `records` 字段携带 CSV / JSON 文本内容(utf8 字符串),与 dataloom 上
89
+ * Import/ExportAdminRecords 命名风格对齐。CLI 端把 Buffer 解码成 utf8
90
+ * 字符串后塞进 envelope 即可。
91
+ */
92
+ async function importData(opts) {
93
+ const client = (0, http_1.getHttpClient)();
94
+ const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/import");
95
+ const reqBody = {
96
+ tableName: opts.tableName,
97
+ format: opts.format,
98
+ records: opts.body.toString("utf8"),
99
+ };
100
+ if (opts.dbBranch !== undefined && opts.dbBranch !== "") {
101
+ reqBody.dbBranch = opts.dbBranch;
102
+ }
103
+ const start = Date.now();
104
+ const response = await client.post(url, reqBody);
105
+ (0, client_1.traceHttp)("POST", url, start, response);
106
+ if (!response.ok) {
107
+ let body = null;
108
+ try {
109
+ body = (await response.json());
110
+ }
111
+ catch {
112
+ // ignore
113
+ }
114
+ if (body)
115
+ (0, client_1.extractData)(body);
116
+ throw new error_1.HttpError(response.status, url, `Failed to import data: ${String(response.status)} ${response.statusText}`);
117
+ }
118
+ // 后端 InnerAdminImportData 响应里 data 直接返 {tableName, recordCount, durationMs}
119
+ const body = (await response.json());
120
+ const data = (0, client_1.extractData)(body);
121
+ return {
122
+ tableName: data.tableName ?? opts.tableName,
123
+ recordCount: data.recordCount ?? 0,
124
+ durationMs: data.durationMs ?? 0,
125
+ };
126
+ }
127
+ // ── db data export → InnerAdminExportData ──
128
+ /**
129
+ * 导出数据。
130
+ * 后端:POST /v1/dataloom/app/{appId}/data/export?tableName=...&format=csv|json&limit=5000&dbBranch=main
131
+ *
132
+ * 所有参数(含 limit)均按 IDL `api.query` 走 query string;HTTP 方法是 POST(对齐
133
+ * inner_api 网关插件路由约定,与 InnerAdminExecuteSQL 同 method)。请求体为空。
134
+ * 响应 body 为原始 CSV/JSON 字节,RecordCount 通过响应头 `X-Miaoda-Record-Count`
135
+ * 回传,错误仍走 HTTP 4xx/5xx + envelope。
136
+ */
137
+ async function exportData(opts) {
138
+ const client = (0, http_1.getHttpClient)();
139
+ const url = (0, client_1.buildInnerUrl)(opts.appId, "/data/export", {
140
+ tableName: opts.tableName,
141
+ format: opts.format,
142
+ limit: String(opts.limit ?? 5000),
143
+ dbBranch: opts.dbBranch,
144
+ });
145
+ // POST + 空 body:所有业务参数都在 query 里
146
+ const start = Date.now();
147
+ const response = await client.request({ method: "POST", url });
148
+ (0, client_1.traceHttp)("POST", url, start, response);
149
+ if (!response.ok) {
150
+ // 错误路径:body 是 JSON envelope
151
+ let body = null;
152
+ try {
153
+ body = (await response.json());
154
+ }
155
+ catch {
156
+ // ignore
157
+ }
158
+ if (body)
159
+ (0, client_1.extractData)(body);
160
+ throw new error_1.HttpError(response.status, url, `Failed to export data: ${String(response.status)} ${response.statusText}`);
161
+ }
162
+ // 成功路径:响应 body 通常是原始 CSV/JSON 字节,但部分错误场景下网关会返
163
+ // HTTP 200 + JSON envelope(status_code != "0"),需要在这里嗅探兜底。
164
+ const contentType = response.headers.get("Content-Type") ??
165
+ (opts.format === "csv" ? "text/csv" : "application/json");
166
+ const ab = await response.arrayBuffer();
167
+ const buf = Buffer.from(new Uint8Array(ab));
168
+ if (buf.length === 0) {
169
+ throw new error_1.AppError("INTERNAL_DB_ERROR", "Empty export response body");
170
+ }
171
+ // Envelope sniff:HTTP 200 + Content-Type 为 application/json + body 解析得到
172
+ // InnerEnvelope 且 status_code 非 "0" 时,按业务错误抛出。
173
+ // 仅 CSV 格式做 sniff —— JSON 格式正常成功响应也是 application/json,会误判。
174
+ if (opts.format === "csv" && /application\/json/i.test(contentType)) {
175
+ try {
176
+ const parsed = JSON.parse(buf.toString("utf8"));
177
+ if (parsed.status_code != null && parsed.status_code !== "0") {
178
+ // 复用 extractData 的错误映射逻辑(throw AppError)
179
+ (0, client_1.extractData)(parsed);
180
+ }
181
+ }
182
+ catch (err) {
183
+ // 已经被 extractData 抛成 AppError → 透传;否则 JSON.parse 失败说明 body
184
+ // 真的是 CSV 文本,继续按成功流程走
185
+ if (err instanceof error_1.AppError)
186
+ throw err;
187
+ }
188
+ }
189
+ // 后端通过响应头回传记录数(避免污染 body);header 缺失或解析失败 → undefined
190
+ const recordCountHeader = response.headers.get("X-Miaoda-Record-Count");
191
+ const parsedCount = recordCountHeader != null ? Number(recordCountHeader) : NaN;
192
+ const recordCount = Number.isFinite(parsedCount) && parsedCount >= 0 ? parsedCount : undefined;
193
+ return {
194
+ tableName: opts.tableName,
195
+ format: opts.format,
196
+ contentType,
197
+ body: buf,
198
+ recordCount,
199
+ };
200
+ }
@@ -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 });