@lark-apaas/miaoda-cli 0.1.2-alpha.d0d2ae1 → 0.1.2-alpha.d5d1878
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 +67 -0
- package/dist/api/db/index.js +3 -1
- package/dist/cli/handlers/db/migration.js +26 -4
- package/dist/cli/handlers/db/recovery.js +99 -46
- package/dist/utils/index.js +3 -1
- package/dist/utils/poll.js +27 -0
- package/package.json +1 -1
package/dist/api/db/api.js
CHANGED
|
@@ -10,7 +10,9 @@ exports.setAuditConfig = setAuditConfig;
|
|
|
10
10
|
exports.listAuditLog = listAuditLog;
|
|
11
11
|
exports.migrationInit = migrationInit;
|
|
12
12
|
exports.migrate = migrate;
|
|
13
|
+
exports.getMigrationStatus = getMigrationStatus;
|
|
13
14
|
exports.recover = recover;
|
|
15
|
+
exports.getRecoveryPreview = getRecoveryPreview;
|
|
14
16
|
exports.getDbQuota = getDbQuota;
|
|
15
17
|
const http_1 = require("../../utils/http");
|
|
16
18
|
const error_1 = require("../../utils/error");
|
|
@@ -33,6 +35,22 @@ async function mapDbHttpError(err, url, ctx,
|
|
|
33
35
|
onErrorBody) {
|
|
34
36
|
if (err instanceof error_1.AppError)
|
|
35
37
|
throw err;
|
|
38
|
+
// 客户端超时 / abort:SdkHttpError 时 response 通常缺失(status=0),落到 throw 时
|
|
39
|
+
// message 形如 'Failed to recover: 0' 让用户看不懂。识别 abort / timeout 关键字
|
|
40
|
+
// 转成专用错误码 + 友好 hint。
|
|
41
|
+
if (err instanceof Error) {
|
|
42
|
+
const msg = err.message.toLowerCase();
|
|
43
|
+
if (msg.includes("aborted") ||
|
|
44
|
+
msg.includes("timeout") ||
|
|
45
|
+
err.name === "AbortError" ||
|
|
46
|
+
err.name === "TimeoutError") {
|
|
47
|
+
throw new error_1.AppError("REQUEST_TIMEOUT", `${ctx}: request timed out`, {
|
|
48
|
+
next_actions: [
|
|
49
|
+
"Server-side async tasks may take up to 60s. Retry the command if the underlying task likely succeeded.",
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
36
54
|
if (err instanceof http_client_1.HttpError) {
|
|
37
55
|
const status = err.response?.status ?? 0;
|
|
38
56
|
const statusText = err.response?.statusText ?? "";
|
|
@@ -484,6 +502,31 @@ async function migrate(opts) {
|
|
|
484
502
|
const respBody = (await response.json());
|
|
485
503
|
return (0, client_1.extractData)(respBody);
|
|
486
504
|
}
|
|
505
|
+
/**
|
|
506
|
+
* 后端:GET /v1/dataloom/app/{appId}/db/migration/status?taskId=...
|
|
507
|
+
* CLI 拿到 migration apply 的 taskId 后定时调本接口,直到 status=success/failed。
|
|
508
|
+
* 网络层超时仍走 mapDbHttpError → 单次 30s;轮询节奏由 CLI handler 自行控制。
|
|
509
|
+
*/
|
|
510
|
+
async function getMigrationStatus(opts) {
|
|
511
|
+
const client = (0, http_1.getHttpClient)();
|
|
512
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/migration/status", {
|
|
513
|
+
taskId: opts.taskId,
|
|
514
|
+
dbBranch: opts.dbBranch,
|
|
515
|
+
});
|
|
516
|
+
const start = Date.now();
|
|
517
|
+
let response;
|
|
518
|
+
try {
|
|
519
|
+
response = await client.get(url);
|
|
520
|
+
(0, client_1.traceHttp)("GET", url, start, response);
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
(0, client_1.traceHttp)("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
|
|
524
|
+
await mapDbHttpError(err, url, "Failed to get migration status");
|
|
525
|
+
throw err; // 不可达
|
|
526
|
+
}
|
|
527
|
+
const body = (await response.json());
|
|
528
|
+
return (0, client_1.extractData)(body);
|
|
529
|
+
}
|
|
487
530
|
// ── db recovery → InnerAdminRecover ──
|
|
488
531
|
/**
|
|
489
532
|
* 后端:POST /v1/dataloom/app/{appId}/db/recovery
|
|
@@ -509,6 +552,30 @@ async function recover(opts) {
|
|
|
509
552
|
const respBody = (await response.json());
|
|
510
553
|
return (0, client_1.extractData)(respBody);
|
|
511
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* 后端:GET /v1/dataloom/app/{appId}/db/recovery/preview?previewRequestId=...
|
|
557
|
+
* CLI 拿到 recovery diff 的 previewRequestId 后定时调本接口直到 previewStatus=success/failed。
|
|
558
|
+
*/
|
|
559
|
+
async function getRecoveryPreview(opts) {
|
|
560
|
+
const client = (0, http_1.getHttpClient)();
|
|
561
|
+
const url = (0, client_1.buildInnerUrl)(opts.appId, "/db/recovery/preview", {
|
|
562
|
+
previewRequestId: opts.previewRequestId,
|
|
563
|
+
dbBranch: opts.dbBranch,
|
|
564
|
+
});
|
|
565
|
+
const start = Date.now();
|
|
566
|
+
let response;
|
|
567
|
+
try {
|
|
568
|
+
response = await client.get(url);
|
|
569
|
+
(0, client_1.traceHttp)("GET", url, start, response);
|
|
570
|
+
}
|
|
571
|
+
catch (err) {
|
|
572
|
+
(0, client_1.traceHttp)("GET", url, start, err instanceof http_client_1.HttpError ? err.response : undefined, err);
|
|
573
|
+
await mapDbHttpError(err, url, "Failed to get recovery preview");
|
|
574
|
+
throw err; // 不可达
|
|
575
|
+
}
|
|
576
|
+
const body = (await response.json());
|
|
577
|
+
return (0, client_1.extractData)(body);
|
|
578
|
+
}
|
|
512
579
|
// ── db quota → InnerAdminGetDbQuota ──
|
|
513
580
|
/**
|
|
514
581
|
* 后端:GET /v1/dataloom/app/{appId}/db/quota?dbBranch=
|
package/dist/api/db/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.pickTableDetail = exports.flattenSchemaList = exports.parseSqlResult = exports.SQLSTATE_MAP = exports.ensureInnerSuccess = exports.buildInnerUrl = exports.getDbQuota = exports.recover = exports.migrate = exports.migrationInit = exports.listAuditLog = exports.setAuditConfig = exports.getAuditStatus = exports.listDDLChangelog = exports.exportData = exports.importData = exports.getSchema = exports.execSql = void 0;
|
|
3
|
+
exports.pickTableDetail = exports.flattenSchemaList = exports.parseSqlResult = exports.SQLSTATE_MAP = exports.ensureInnerSuccess = exports.buildInnerUrl = exports.getDbQuota = exports.getRecoveryPreview = exports.recover = exports.getMigrationStatus = exports.migrate = exports.migrationInit = exports.listAuditLog = exports.setAuditConfig = exports.getAuditStatus = exports.listDDLChangelog = exports.exportData = exports.importData = exports.getSchema = exports.execSql = void 0;
|
|
4
4
|
var api_1 = require("./api");
|
|
5
5
|
Object.defineProperty(exports, "execSql", { enumerable: true, get: function () { return api_1.execSql; } });
|
|
6
6
|
Object.defineProperty(exports, "getSchema", { enumerable: true, get: function () { return api_1.getSchema; } });
|
|
@@ -12,7 +12,9 @@ Object.defineProperty(exports, "setAuditConfig", { enumerable: true, get: functi
|
|
|
12
12
|
Object.defineProperty(exports, "listAuditLog", { enumerable: true, get: function () { return api_1.listAuditLog; } });
|
|
13
13
|
Object.defineProperty(exports, "migrationInit", { enumerable: true, get: function () { return api_1.migrationInit; } });
|
|
14
14
|
Object.defineProperty(exports, "migrate", { enumerable: true, get: function () { return api_1.migrate; } });
|
|
15
|
+
Object.defineProperty(exports, "getMigrationStatus", { enumerable: true, get: function () { return api_1.getMigrationStatus; } });
|
|
15
16
|
Object.defineProperty(exports, "recover", { enumerable: true, get: function () { return api_1.recover; } });
|
|
17
|
+
Object.defineProperty(exports, "getRecoveryPreview", { enumerable: true, get: function () { return api_1.getRecoveryPreview; } });
|
|
16
18
|
Object.defineProperty(exports, "getDbQuota", { enumerable: true, get: function () { return api_1.getDbQuota; } });
|
|
17
19
|
var client_1 = require("./client");
|
|
18
20
|
Object.defineProperty(exports, "buildInnerUrl", { enumerable: true, get: function () { return client_1.buildInnerUrl; } });
|
|
@@ -44,6 +44,7 @@ const api = __importStar(require("../../../api/index"));
|
|
|
44
44
|
const shared_1 = require("../../../cli/commands/shared");
|
|
45
45
|
const error_1 = require("../../../utils/error");
|
|
46
46
|
const output_1 = require("../../../utils/output");
|
|
47
|
+
const poll_1 = require("../../../utils/poll");
|
|
47
48
|
const render_1 = require("../../../utils/render");
|
|
48
49
|
async function handleDbMigrationInit(opts) {
|
|
49
50
|
const appId = (0, shared_1.resolveAppId)(opts);
|
|
@@ -136,21 +137,42 @@ async function handleDbMigrationApply(opts) {
|
|
|
136
137
|
catch (err) {
|
|
137
138
|
throw decorateMigrationError(err);
|
|
138
139
|
}
|
|
140
|
+
// dataloom 立即返 taskId(apply 实际是异步流水线)。CLI 自己 poll 直到 success/failed,
|
|
141
|
+
// 避免单次 HTTP 长连接 30s+ 被网关 / SDK 中断。
|
|
142
|
+
if (result.taskId === undefined || result.taskId === "") {
|
|
143
|
+
throw new error_1.AppError("INTERNAL_DB_ERROR", "migration apply did not return taskId");
|
|
144
|
+
}
|
|
145
|
+
const taskId = result.taskId;
|
|
146
|
+
const final = await (0, poll_1.pollUntilDone)({
|
|
147
|
+
label: "migration apply",
|
|
148
|
+
intervalMs: 1000,
|
|
149
|
+
fetch: () => api.db.getMigrationStatus({ appId, taskId }),
|
|
150
|
+
isDone: (cur) => {
|
|
151
|
+
// 同 recovery preview,dataloom 上下游枚举大小写不完全统一,客户端归一兜底。
|
|
152
|
+
const status = cur.status.toLowerCase();
|
|
153
|
+
if (status === "success")
|
|
154
|
+
return { done: true, value: cur };
|
|
155
|
+
if (status === "failed") {
|
|
156
|
+
throw new error_1.AppError("DB_API_k_dl_1300030", cur.errorMessage ?? `migration apply failed (taskId=${taskId})`);
|
|
157
|
+
}
|
|
158
|
+
return { done: false };
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
const appliedCount = final.changesApplied ?? result.changes.length;
|
|
139
162
|
if ((0, output_1.isJsonMode)()) {
|
|
140
163
|
// PRD:{"status": "applied", "from": "dev", "to": "online", "changes_applied": 2}
|
|
141
164
|
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
|
|
142
|
-
status:
|
|
165
|
+
status: "applied",
|
|
143
166
|
from: result.from,
|
|
144
167
|
to: result.to,
|
|
145
|
-
changesApplied:
|
|
168
|
+
changesApplied: appliedCount,
|
|
146
169
|
}));
|
|
147
170
|
return;
|
|
148
171
|
}
|
|
149
172
|
const tty = (0, render_1.isStdoutTty)();
|
|
150
173
|
const prefix = tty ? "✓" : "OK";
|
|
151
174
|
const arrow = tty ? "→" : "->";
|
|
152
|
-
|
|
153
|
-
(0, output_1.emit)(`${prefix} Applied ${result.from} ${arrow} ${result.to} (${String(applied)} changes)`);
|
|
175
|
+
(0, output_1.emit)(`${prefix} Applied ${result.from} ${arrow} ${result.to} (${String(appliedCount)} changes)`);
|
|
154
176
|
}
|
|
155
177
|
// ── helpers ──
|
|
156
178
|
// PRD diff 输出:
|
|
@@ -43,34 +43,27 @@ const api = __importStar(require("../../../api/index"));
|
|
|
43
43
|
const shared_1 = require("../../../cli/commands/shared");
|
|
44
44
|
const error_1 = require("../../../utils/error");
|
|
45
45
|
const output_1 = require("../../../utils/output");
|
|
46
|
+
const poll_1 = require("../../../utils/poll");
|
|
46
47
|
const render_1 = require("../../../utils/render");
|
|
47
48
|
// ── recovery diff ──
|
|
49
|
+
//
|
|
50
|
+
// PITR preview 是异步任务:dataloom 立即返 previewRequestId,CLI 自己 poll 直到 success/failed。
|
|
51
|
+
// 失败时 dataloom 已用 translateRestoreErr 把中文窗口错误翻成 PRD 英文 + k_dl_1300036 code,
|
|
52
|
+
// CLI 走 decorateRecoveryError 补 hint。
|
|
48
53
|
async function handleDbRecoveryDiff(target, opts) {
|
|
49
54
|
const appId = (0, shared_1.resolveAppId)(opts);
|
|
50
55
|
const ts = normalizeTimestamp(target);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
result = await api.db.recover({ appId, target: ts, dryRun: true });
|
|
54
|
-
}
|
|
55
|
-
catch (err) {
|
|
56
|
-
throw decorateRecoveryError(err);
|
|
57
|
-
}
|
|
58
|
-
renderDiff(result);
|
|
56
|
+
const preview = await runRecoveryPreview(appId, ts);
|
|
57
|
+
renderDiff(ts, preview);
|
|
59
58
|
}
|
|
60
59
|
async function handleDbRecoveryApply(target, opts) {
|
|
61
60
|
const appId = (0, shared_1.resolveAppId)(opts);
|
|
62
61
|
const ts = normalizeTimestamp(target);
|
|
63
62
|
// PITR 高危:TTY 下先 diff 给用户审;--yes 直接执行
|
|
64
63
|
if (!opts.yes && !(0, output_1.isJsonMode)()) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
throw decorateRecoveryError(err);
|
|
71
|
-
}
|
|
72
|
-
renderDiff(preview);
|
|
73
|
-
const ok = await confirm(`? Restore database to ${preview.target}? This will overwrite current data. (y/N) `);
|
|
64
|
+
const preview = await runRecoveryPreview(appId, ts);
|
|
65
|
+
renderDiff(ts, preview);
|
|
66
|
+
const ok = await confirm(`? Restore database to ${ts}? This will overwrite current data. (y/N) `);
|
|
74
67
|
if (!ok) {
|
|
75
68
|
(0, output_1.emit)("Aborted.");
|
|
76
69
|
return;
|
|
@@ -84,37 +77,87 @@ async function handleDbRecoveryApply(target, opts) {
|
|
|
84
77
|
throw decorateRecoveryError(err);
|
|
85
78
|
}
|
|
86
79
|
if ((0, output_1.isJsonMode)()) {
|
|
87
|
-
// PRD:{"status": "restored", "target": "...", "
|
|
80
|
+
// PRD:{"status": "restored", "target": "...", "elapsed_seconds": 30}
|
|
81
|
+
// tables_affected 来源于 preview,apply 路径不再回带,由 CLI 不强求字段。
|
|
88
82
|
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
|
|
89
83
|
status: result.status ?? "restored",
|
|
90
84
|
target: result.target,
|
|
91
|
-
tablesAffected: result.tablesAffected,
|
|
92
85
|
elapsedSeconds: result.elapsedSeconds ?? 0,
|
|
93
86
|
}));
|
|
94
87
|
return;
|
|
95
88
|
}
|
|
96
89
|
const tty = (0, render_1.isStdoutTty)();
|
|
97
90
|
const prefix = tty ? "✓" : "OK";
|
|
98
|
-
(0, output_1.emit)(`${prefix} Database
|
|
99
|
-
`(${String(result.
|
|
91
|
+
(0, output_1.emit)(`${prefix} Database restore triggered for ${result.target} ` +
|
|
92
|
+
`(${String(result.elapsedSeconds ?? 0)}s elapsed)`);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 触发 PITR 预览任务并轮询到终态:
|
|
96
|
+
* 1. POST /db/recovery dryRun=true → previewRequestId
|
|
97
|
+
* 2. GET /db/recovery/preview?previewRequestId 直到 previewStatus=success/failed
|
|
98
|
+
*
|
|
99
|
+
* 错误透传:业务错误(窗口超限 / 格式错)由 dataloom 直接 throw,CLI 在 catch 里
|
|
100
|
+
* 补 hint。failed 状态下 dataloom 已把 errorMessage 翻好,CLI 包成 k_dl_1300036
|
|
101
|
+
* 让 decorateRecoveryError 命中窗口超限 hint。
|
|
102
|
+
*/
|
|
103
|
+
async function runRecoveryPreview(appId, ts) {
|
|
104
|
+
let triggered;
|
|
105
|
+
try {
|
|
106
|
+
triggered = await api.db.recover({ appId, target: ts, dryRun: true });
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
throw decorateRecoveryError(err);
|
|
110
|
+
}
|
|
111
|
+
if (triggered.previewRequestId === undefined || triggered.previewRequestId === "") {
|
|
112
|
+
throw new error_1.AppError("INTERNAL_DB_ERROR", "recovery diff did not return previewRequestId");
|
|
113
|
+
}
|
|
114
|
+
const previewRequestId = triggered.previewRequestId;
|
|
115
|
+
try {
|
|
116
|
+
return await (0, poll_1.pollUntilDone)({
|
|
117
|
+
label: "recovery preview",
|
|
118
|
+
intervalMs: 1000,
|
|
119
|
+
fetch: () => api.db.getRecoveryPreview({ appId, previewRequestId }),
|
|
120
|
+
isDone: (cur) => {
|
|
121
|
+
// dataloom 内部 pgsvc 返回首字母大写枚举(Pending/Running/Success/Failed),
|
|
122
|
+
// 但 admin-inner thrift 契约里写的是小写。客户端做大小写归一兜底,避免依赖
|
|
123
|
+
// 上游 case 一致性导致死循环。
|
|
124
|
+
const status = cur.previewStatus.toLowerCase();
|
|
125
|
+
if (status === "success")
|
|
126
|
+
return { done: true, value: cur };
|
|
127
|
+
if (status === "failed") {
|
|
128
|
+
// 复用 k_dl_1300036(窗口超限 / 预览失败的统一对外码),让 decorateRecoveryError
|
|
129
|
+
// 命中后给 PITR 7 天窗口的 hint。errorMessage 优先用 dataloom 翻好的 PRD 文案。
|
|
130
|
+
throw new error_1.AppError("DB_API_k_dl_1300036", cur.errorMessage ?? "recovery preview failed");
|
|
131
|
+
}
|
|
132
|
+
return { done: false };
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
throw decorateRecoveryError(err);
|
|
138
|
+
}
|
|
100
139
|
}
|
|
101
140
|
// ── helpers ──
|
|
102
141
|
/**
|
|
103
|
-
* 把用户传入的时间统一成 ISO 8601 UTC。
|
|
104
|
-
*
|
|
142
|
+
* 把用户传入的时间统一成 ISO 8601 UTC。PRD 只接受两种格式:
|
|
143
|
+
* - `YYYY-MM-DD`(按 UTC 0 点解释)
|
|
144
|
+
* - 完整 ISO 8601 `YYYY-MM-DDTHH:MM:SS[Z|±HH:MM]`
|
|
145
|
+
*
|
|
146
|
+
* 严格 regex 匹配 + 不用 new Date(input) 兜底——Node Date 解析 `2026/04/15` 这种
|
|
147
|
+
* 非 ISO 字符串居然成功(落到本地时区),绕过格式校验把脏值送到后端,导致用户拿到
|
|
148
|
+
* 「窗口超限」错误而不是「格式错误」。PRD 明确这两种格式以外的都应该报 INVALID_TIMESTAMP。
|
|
105
149
|
*/
|
|
106
150
|
function normalizeTimestamp(input) {
|
|
107
|
-
|
|
151
|
+
// PRD hint 文案对齐 PRD 截图(同时是 dataloom 端 ErrInvalidTimestamp 的 hint)
|
|
152
|
+
const FORMAT_HINT = "Use ISO 8601 format, e.g. 2026-04-15 or 2026-04-15T10:00:00Z";
|
|
108
153
|
if (input === "") {
|
|
109
154
|
throw new error_1.AppError("ARGS_INVALID", "target timestamp is required", {
|
|
110
155
|
next_actions: ["Usage: miaoda db recovery diff|apply <timestamp>"],
|
|
111
156
|
});
|
|
112
157
|
}
|
|
158
|
+
// 仅日期:YYYY-MM-DD → 按 UTC 0 点
|
|
113
159
|
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?$/.test(input)) {
|
|
117
|
-
const d = new Date(input.replace(" ", "T"));
|
|
160
|
+
const d = new Date(`${input}T00:00:00Z`);
|
|
118
161
|
if (Number.isNaN(d.getTime())) {
|
|
119
162
|
throw new error_1.AppError("INVALID_TIMESTAMP", `Invalid timestamp format '${input}'`, {
|
|
120
163
|
next_actions: [FORMAT_HINT],
|
|
@@ -122,13 +165,20 @@ function normalizeTimestamp(input) {
|
|
|
122
165
|
}
|
|
123
166
|
return d.toISOString();
|
|
124
167
|
}
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
168
|
+
// 完整 ISO 8601 datetime:T 分隔,秒可选 ms,Z 或 ±HH:MM 时区
|
|
169
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/.test(input)) {
|
|
170
|
+
const d = new Date(input);
|
|
171
|
+
if (Number.isNaN(d.getTime())) {
|
|
172
|
+
throw new error_1.AppError("INVALID_TIMESTAMP", `Invalid timestamp format '${input}'`, {
|
|
173
|
+
next_actions: [FORMAT_HINT],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return d.toISOString();
|
|
130
177
|
}
|
|
131
|
-
|
|
178
|
+
// 其它一律按格式错误拒绝(`2026/04/15` / `2026.04.15` / `2026年4月15日` 等都走这里)
|
|
179
|
+
throw new error_1.AppError("INVALID_TIMESTAMP", `Invalid timestamp format '${input}'`, {
|
|
180
|
+
next_actions: [FORMAT_HINT],
|
|
181
|
+
});
|
|
132
182
|
}
|
|
133
183
|
// PRD diff 输出(结构化 prose):
|
|
134
184
|
// Recovery preview (→ 2026-04-15T10:00:00Z):
|
|
@@ -138,12 +188,15 @@ function normalizeTimestamp(input) {
|
|
|
138
188
|
// orders: table will be restored (was dropped at 10:25:00)
|
|
139
189
|
//
|
|
140
190
|
// estimated time: ~30s
|
|
141
|
-
function renderDiff(
|
|
191
|
+
function renderDiff(target, preview) {
|
|
192
|
+
const changes = preview.changes ?? [];
|
|
193
|
+
const tablesAffected = preview.tablesAffected ?? changes.length;
|
|
194
|
+
const estimated = preview.estimatedSeconds;
|
|
142
195
|
if ((0, output_1.isJsonMode)()) {
|
|
143
196
|
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
|
|
144
|
-
target
|
|
145
|
-
tablesAffected
|
|
146
|
-
changes:
|
|
197
|
+
target,
|
|
198
|
+
tablesAffected,
|
|
199
|
+
changes: changes.map((c) => ({
|
|
147
200
|
table: c.table,
|
|
148
201
|
inserted: c.inserted,
|
|
149
202
|
deleted: c.deleted,
|
|
@@ -151,28 +204,28 @@ function renderDiff(result) {
|
|
|
151
204
|
action: c.action,
|
|
152
205
|
droppedAt: c.droppedAt,
|
|
153
206
|
})),
|
|
154
|
-
estimatedSeconds:
|
|
207
|
+
estimatedSeconds: estimated ?? 0,
|
|
155
208
|
}));
|
|
156
209
|
return;
|
|
157
210
|
}
|
|
158
211
|
const tty = (0, render_1.isStdoutTty)();
|
|
159
212
|
const arrow = tty ? "→" : "->";
|
|
160
|
-
if (
|
|
161
|
-
(0, output_1.emit)(`Recovery preview (${arrow} ${
|
|
213
|
+
if (changes.length === 0) {
|
|
214
|
+
(0, output_1.emit)(`Recovery preview (${arrow} ${target}):\n\n` +
|
|
162
215
|
` No changes — database is already at this state.`);
|
|
163
216
|
return;
|
|
164
217
|
}
|
|
165
218
|
const lines = [
|
|
166
|
-
`Recovery preview (${arrow} ${
|
|
219
|
+
`Recovery preview (${arrow} ${target}):`,
|
|
167
220
|
"",
|
|
168
|
-
` tables affected: ${String(
|
|
221
|
+
` tables affected: ${String(tablesAffected)}`,
|
|
169
222
|
];
|
|
170
|
-
for (const c of
|
|
223
|
+
for (const c of changes) {
|
|
171
224
|
lines.push(` ${c.table}: ${describeChange(c)}`);
|
|
172
225
|
}
|
|
173
|
-
if (
|
|
226
|
+
if (estimated !== undefined) {
|
|
174
227
|
lines.push("");
|
|
175
|
-
lines.push(` estimated time: ~${String(
|
|
228
|
+
lines.push(` estimated time: ~${String(estimated)}s`);
|
|
176
229
|
}
|
|
177
230
|
(0, output_1.emit)(lines.join("\n"));
|
|
178
231
|
}
|
package/dist/utils/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.setRuntimeHttpClient = exports.setHttpClient = exports.resetHttpClient = exports.getRuntimeHttpClient = exports.getHttpClient = exports.log = exports.debug = exports.isJsonMode = exports.emitError = exports.emit = exports.getLogId = exports.generateLogId = exports.initConfigFromOpts = exports.resetConfig = exports.setConfig = exports.getConfig = exports.HttpError = exports.AppError = void 0;
|
|
3
|
+
exports.pollUntilDone = exports.setRuntimeHttpClient = exports.setHttpClient = exports.resetHttpClient = exports.getRuntimeHttpClient = exports.getHttpClient = exports.log = exports.debug = exports.isJsonMode = exports.emitError = exports.emit = exports.getLogId = exports.generateLogId = exports.initConfigFromOpts = exports.resetConfig = exports.setConfig = exports.getConfig = exports.HttpError = exports.AppError = void 0;
|
|
4
4
|
var error_1 = require("./error");
|
|
5
5
|
Object.defineProperty(exports, "AppError", { enumerable: true, get: function () { return error_1.AppError; } });
|
|
6
6
|
Object.defineProperty(exports, "HttpError", { enumerable: true, get: function () { return error_1.HttpError; } });
|
|
@@ -25,3 +25,5 @@ Object.defineProperty(exports, "getRuntimeHttpClient", { enumerable: true, get:
|
|
|
25
25
|
Object.defineProperty(exports, "resetHttpClient", { enumerable: true, get: function () { return http_1.resetHttpClient; } });
|
|
26
26
|
Object.defineProperty(exports, "setHttpClient", { enumerable: true, get: function () { return http_1.setHttpClient; } });
|
|
27
27
|
Object.defineProperty(exports, "setRuntimeHttpClient", { enumerable: true, get: function () { return http_1.setRuntimeHttpClient; } });
|
|
28
|
+
var poll_1 = require("./poll");
|
|
29
|
+
Object.defineProperty(exports, "pollUntilDone", { enumerable: true, get: function () { return poll_1.pollUntilDone; } });
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pollUntilDone = pollUntilDone;
|
|
4
|
+
const error_1 = require("../utils/error");
|
|
5
|
+
async function pollUntilDone(opts) {
|
|
6
|
+
const interval = opts.intervalMs ?? 1000;
|
|
7
|
+
const timeout = opts.timeoutMs ?? 300_000;
|
|
8
|
+
const deadline = Date.now() + timeout;
|
|
9
|
+
// 立即拉一次(绝大多数轻量任务在 dataloom 端已是同步语义,第一次 fetch 就能拿到 success)
|
|
10
|
+
for (;;) {
|
|
11
|
+
const cur = await opts.fetch();
|
|
12
|
+
const verdict = opts.isDone(cur);
|
|
13
|
+
if (verdict.done)
|
|
14
|
+
return verdict.value;
|
|
15
|
+
if (Date.now() + interval > deadline) {
|
|
16
|
+
throw new error_1.AppError("TASK_TIMEOUT", `${opts.label} did not complete within ${String(Math.round(timeout / 1000))}s`, {
|
|
17
|
+
next_actions: [
|
|
18
|
+
"The task may still be running server-side. Retry the command, or check `miaoda db migration diff` to verify final state.",
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
await sleep(interval);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function sleep(ms) {
|
|
26
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
27
|
+
}
|