@lark-apaas/miaoda-cli 0.1.2-alpha.4e370b6 → 0.1.2-alpha.514221e
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/cli/handlers/db/recovery.js +67 -16
- package/package.json +1 -1
|
@@ -48,7 +48,13 @@ const render_1 = require("../../../utils/render");
|
|
|
48
48
|
async function handleDbRecoveryDiff(target, opts) {
|
|
49
49
|
const appId = (0, shared_1.resolveAppId)(opts);
|
|
50
50
|
const ts = normalizeTimestamp(target);
|
|
51
|
-
|
|
51
|
+
let result;
|
|
52
|
+
try {
|
|
53
|
+
result = await api.db.recover({ appId, target: ts, dryRun: true });
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
throw decorateRecoveryError(err);
|
|
57
|
+
}
|
|
52
58
|
renderDiff(result);
|
|
53
59
|
}
|
|
54
60
|
async function handleDbRecoveryApply(target, opts) {
|
|
@@ -56,7 +62,13 @@ async function handleDbRecoveryApply(target, opts) {
|
|
|
56
62
|
const ts = normalizeTimestamp(target);
|
|
57
63
|
// PITR 高危:TTY 下先 diff 给用户审;--yes 直接执行
|
|
58
64
|
if (!opts.yes && !(0, output_1.isJsonMode)()) {
|
|
59
|
-
|
|
65
|
+
let preview;
|
|
66
|
+
try {
|
|
67
|
+
preview = await api.db.recover({ appId, target: ts, dryRun: true });
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
throw decorateRecoveryError(err);
|
|
71
|
+
}
|
|
60
72
|
renderDiff(preview);
|
|
61
73
|
const ok = await confirm(`? Restore database to ${preview.target}? This will overwrite current data. (y/N) `);
|
|
62
74
|
if (!ok) {
|
|
@@ -64,7 +76,13 @@ async function handleDbRecoveryApply(target, opts) {
|
|
|
64
76
|
return;
|
|
65
77
|
}
|
|
66
78
|
}
|
|
67
|
-
|
|
79
|
+
let result;
|
|
80
|
+
try {
|
|
81
|
+
result = await api.db.recover({ appId, target: ts, dryRun: false });
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
throw decorateRecoveryError(err);
|
|
85
|
+
}
|
|
68
86
|
if ((0, output_1.isJsonMode)()) {
|
|
69
87
|
// PRD:{"status": "restored", "target": "...", "tables_affected": 2, "elapsed_seconds": 30}
|
|
70
88
|
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
|
|
@@ -82,21 +100,25 @@ async function handleDbRecoveryApply(target, opts) {
|
|
|
82
100
|
}
|
|
83
101
|
// ── helpers ──
|
|
84
102
|
/**
|
|
85
|
-
* 把用户传入的时间统一成 ISO 8601 UTC。
|
|
86
|
-
*
|
|
103
|
+
* 把用户传入的时间统一成 ISO 8601 UTC。PRD 只接受两种格式:
|
|
104
|
+
* - `YYYY-MM-DD`(按 UTC 0 点解释)
|
|
105
|
+
* - 完整 ISO 8601 `YYYY-MM-DDTHH:MM:SS[Z|±HH:MM]`
|
|
106
|
+
*
|
|
107
|
+
* 严格 regex 匹配 + 不用 new Date(input) 兜底——Node Date 解析 `2026/04/15` 这种
|
|
108
|
+
* 非 ISO 字符串居然成功(落到本地时区),绕过格式校验把脏值送到后端,导致用户拿到
|
|
109
|
+
* 「窗口超限」错误而不是「格式错误」。PRD 明确这两种格式以外的都应该报 INVALID_TIMESTAMP。
|
|
87
110
|
*/
|
|
88
111
|
function normalizeTimestamp(input) {
|
|
89
|
-
|
|
112
|
+
// PRD hint 文案对齐 PRD 截图(同时是 dataloom 端 ErrInvalidTimestamp 的 hint)
|
|
113
|
+
const FORMAT_HINT = "Use ISO 8601 format, e.g. 2026-04-15 or 2026-04-15T10:00:00Z";
|
|
90
114
|
if (input === "") {
|
|
91
115
|
throw new error_1.AppError("ARGS_INVALID", "target timestamp is required", {
|
|
92
116
|
next_actions: ["Usage: miaoda db recovery diff|apply <timestamp>"],
|
|
93
117
|
});
|
|
94
118
|
}
|
|
119
|
+
// 仅日期:YYYY-MM-DD → 按 UTC 0 点
|
|
95
120
|
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?$/.test(input)) {
|
|
99
|
-
const d = new Date(input.replace(" ", "T"));
|
|
121
|
+
const d = new Date(`${input}T00:00:00Z`);
|
|
100
122
|
if (Number.isNaN(d.getTime())) {
|
|
101
123
|
throw new error_1.AppError("INVALID_TIMESTAMP", `Invalid timestamp format '${input}'`, {
|
|
102
124
|
next_actions: [FORMAT_HINT],
|
|
@@ -104,13 +126,20 @@ function normalizeTimestamp(input) {
|
|
|
104
126
|
}
|
|
105
127
|
return d.toISOString();
|
|
106
128
|
}
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
// 完整 ISO 8601 datetime:T 分隔,秒可选 ms,Z 或 ±HH:MM 时区
|
|
130
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/.test(input)) {
|
|
131
|
+
const d = new Date(input);
|
|
132
|
+
if (Number.isNaN(d.getTime())) {
|
|
133
|
+
throw new error_1.AppError("INVALID_TIMESTAMP", `Invalid timestamp format '${input}'`, {
|
|
134
|
+
next_actions: [FORMAT_HINT],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return d.toISOString();
|
|
112
138
|
}
|
|
113
|
-
|
|
139
|
+
// 其它一律按格式错误拒绝(`2026/04/15` / `2026.04.15` / `2026年4月15日` 等都走这里)
|
|
140
|
+
throw new error_1.AppError("INVALID_TIMESTAMP", `Invalid timestamp format '${input}'`, {
|
|
141
|
+
next_actions: [FORMAT_HINT],
|
|
142
|
+
});
|
|
114
143
|
}
|
|
115
144
|
// PRD diff 输出(结构化 prose):
|
|
116
145
|
// Recovery preview (→ 2026-04-15T10:00:00Z):
|
|
@@ -177,6 +206,28 @@ function describeChange(c) {
|
|
|
177
206
|
parts.push(`~${String(c.modified)} rows modified`);
|
|
178
207
|
return parts.length === 0 ? "no changes" : parts.join(", ");
|
|
179
208
|
}
|
|
209
|
+
// decorateRecoveryError 给 recovery 路径上的几个错误码补 PRD 规定的 hint。
|
|
210
|
+
// dataloom 后端只返 message + code,hint 由 CLI 端按错误码映射;其它错误码原样透传。
|
|
211
|
+
function decorateRecoveryError(err) {
|
|
212
|
+
if (!(err instanceof error_1.AppError))
|
|
213
|
+
return err;
|
|
214
|
+
switch (err.code) {
|
|
215
|
+
case "DB_API_k_dl_1300036":
|
|
216
|
+
// 窗口超限:引导用户检查 last migration apply 时间
|
|
217
|
+
return new error_1.AppError(err.code, err.message, {
|
|
218
|
+
next_actions: [
|
|
219
|
+
"PITR window is up to 7 days back, limited by your last `db migration apply` time.",
|
|
220
|
+
],
|
|
221
|
+
});
|
|
222
|
+
case "DB_API_k_dl_1300038":
|
|
223
|
+
// 时间格式错误:引导 ISO 8601
|
|
224
|
+
return new error_1.AppError(err.code, err.message, {
|
|
225
|
+
next_actions: ["Use ISO 8601 format, e.g. 2026-04-15 or 2026-04-15T10:00:00Z"],
|
|
226
|
+
});
|
|
227
|
+
default:
|
|
228
|
+
return err;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
180
231
|
async function confirm(prompt) {
|
|
181
232
|
const rl = node_readline_1.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
182
233
|
return new Promise((resolve) => {
|