@lark-apaas/miaoda-cli 0.1.2 → 0.1.3-alpha.4bf312e
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/README.md +8 -7
- package/dist/api/app/api.js +25 -0
- package/dist/api/app/index.js +15 -0
- package/dist/api/app/schemas.js +79 -0
- package/dist/api/app/types.js +58 -0
- package/dist/api/db/api.js +317 -6
- package/dist/api/db/client.js +36 -0
- package/dist/api/db/index.js +11 -1
- package/dist/api/deploy/api.js +60 -0
- package/dist/api/deploy/index.js +16 -0
- package/dist/api/deploy/schemas.js +105 -0
- package/dist/api/deploy/types.js +22 -0
- package/dist/api/file/api.js +15 -0
- package/dist/api/file/index.js +2 -1
- package/dist/api/index.js +7 -1
- package/dist/api/observability/api.js +52 -0
- package/dist/api/observability/index.js +16 -0
- package/dist/api/observability/schemas.js +60 -0
- package/dist/api/observability/types.js +27 -0
- package/dist/cli/commands/app/index.js +62 -0
- package/dist/cli/commands/db/index.js +440 -5
- package/dist/cli/commands/deploy/index.js +155 -0
- package/dist/cli/commands/file/index.js +13 -5
- package/dist/cli/commands/index.js +10 -6
- package/dist/cli/commands/observability/index.js +240 -0
- package/dist/cli/commands/shared.js +83 -7
- package/dist/cli/handlers/app/get.js +47 -0
- package/dist/cli/handlers/app/index.js +7 -0
- package/dist/cli/handlers/app/update.js +59 -0
- package/dist/cli/handlers/db/audit.js +285 -0
- package/dist/cli/handlers/db/changelog.js +117 -0
- package/dist/cli/handlers/db/data.js +3 -4
- package/dist/cli/handlers/db/index.js +17 -1
- package/dist/cli/handlers/db/migration.js +235 -0
- package/dist/cli/handlers/db/quota.js +68 -0
- package/dist/cli/handlers/db/recovery.js +328 -0
- package/dist/cli/handlers/db/schema.js +2 -3
- package/dist/cli/handlers/db/sql.js +1 -2
- package/dist/cli/handlers/deploy/deploy.js +84 -0
- package/dist/cli/handlers/deploy/error-log.js +60 -0
- package/dist/cli/handlers/deploy/format.js +39 -0
- package/dist/cli/handlers/deploy/get.js +71 -0
- package/dist/cli/handlers/deploy/helpers.js +41 -0
- package/dist/cli/handlers/deploy/history.js +70 -0
- package/dist/cli/handlers/deploy/index.js +14 -0
- package/dist/cli/handlers/deploy/polling.js +162 -0
- package/dist/cli/handlers/file/cp.js +1 -2
- package/dist/cli/handlers/file/index.js +3 -1
- package/dist/cli/handlers/file/ls.js +1 -2
- package/dist/cli/handlers/file/quota.js +66 -0
- package/dist/cli/handlers/file/rm.js +1 -2
- package/dist/cli/handlers/file/sign.js +1 -2
- package/dist/cli/handlers/file/stat.js +1 -2
- package/dist/cli/handlers/observability/analytics.js +212 -0
- package/dist/cli/handlers/observability/helpers.js +66 -0
- package/dist/cli/handlers/observability/index.js +12 -0
- package/dist/cli/handlers/observability/log.js +94 -0
- package/dist/cli/handlers/observability/metric.js +208 -0
- package/dist/cli/handlers/observability/trace.js +102 -0
- package/dist/main.js +6 -2
- package/dist/utils/args.js +8 -0
- package/dist/utils/devops-error.js +28 -0
- package/dist/utils/git.js +29 -0
- package/dist/utils/http.js +118 -0
- package/dist/utils/index.js +15 -1
- package/dist/utils/output.js +360 -7
- package/dist/utils/poll.js +27 -0
- package/dist/utils/time.js +203 -0
- package/package.json +7 -5
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.handleDbMigrationInit = handleDbMigrationInit;
|
|
40
|
+
exports.handleDbMigrationDiff = handleDbMigrationDiff;
|
|
41
|
+
exports.handleDbMigrationApply = handleDbMigrationApply;
|
|
42
|
+
const node_readline_1 = __importDefault(require("node:readline"));
|
|
43
|
+
const api = __importStar(require("../../../api/index"));
|
|
44
|
+
const shared_1 = require("../../../cli/commands/shared");
|
|
45
|
+
const error_1 = require("../../../utils/error");
|
|
46
|
+
const output_1 = require("../../../utils/output");
|
|
47
|
+
const poll_1 = require("../../../utils/poll");
|
|
48
|
+
const render_1 = require("../../../utils/render");
|
|
49
|
+
async function handleDbMigrationInit(opts) {
|
|
50
|
+
const appId = (0, shared_1.resolveAppId)(opts);
|
|
51
|
+
// 不可逆操作,TTY 默认要求 y/N;--yes 跳过
|
|
52
|
+
if (!opts.yes && !(0, output_1.isJsonMode)()) {
|
|
53
|
+
// 文案对齐 PRD:先讲不可逆,再问是否初始化;syncData 时追加一句而不是塞进同一句话
|
|
54
|
+
const suffix = opts.syncData ? " (existing data will be copied to dev)" : "";
|
|
55
|
+
const ok = await confirm(`? This action is irreversible. Initialize multi-env (dev / online)${suffix}? (y/N) `);
|
|
56
|
+
if (!ok) {
|
|
57
|
+
(0, output_1.emit)("Aborted.");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
let result;
|
|
62
|
+
try {
|
|
63
|
+
result = await api.db.migrationInit({ appId, syncData: opts.syncData });
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
// PRD: 重复 init 报错带 hint,引导用户去 diff 看待发布变更
|
|
67
|
+
if (err instanceof error_1.AppError && err.code === "DB_API_k_dl_1300034") {
|
|
68
|
+
throw new error_1.AppError(err.code, err.message, {
|
|
69
|
+
next_actions: ["Run `miaoda db migration diff` to view pending changes."],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
if ((0, output_1.isJsonMode)()) {
|
|
75
|
+
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)(result));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// PRD 单行格式(不用 key:value 表格):
|
|
79
|
+
// 默认 ✓ / OK: "Multi-env initialized (dev / online)"
|
|
80
|
+
// --sync-data: "Multi-env initialized, data synced to dev"
|
|
81
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
82
|
+
const prefix = tty ? "✓" : "OK";
|
|
83
|
+
const body = result.dataSynced
|
|
84
|
+
? "Multi-env initialized, data synced to dev"
|
|
85
|
+
: `Multi-env initialized (${result.environments.join(" / ")})`;
|
|
86
|
+
(0, output_1.emit)(`${prefix} ${body}`);
|
|
87
|
+
}
|
|
88
|
+
async function handleDbMigrationDiff(opts) {
|
|
89
|
+
const appId = (0, shared_1.resolveAppId)(opts);
|
|
90
|
+
let result;
|
|
91
|
+
try {
|
|
92
|
+
result = await api.db.migrate({ appId, dryRun: true });
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
throw decorateMigrationError(err);
|
|
96
|
+
}
|
|
97
|
+
// PRD: diff 在无待发布变更时报错带 hint,而不是渲染空列表
|
|
98
|
+
if (result.changes.length === 0) {
|
|
99
|
+
throw new error_1.AppError("DB_API_k_dl_1300035", `No pending changes between ${result.from} and ${result.to}`, {
|
|
100
|
+
next_actions: [
|
|
101
|
+
'Make schema changes in dev first (e.g. `miaoda db sql "ALTER TABLE ..." --env dev`)',
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
renderDiff(result);
|
|
106
|
+
}
|
|
107
|
+
async function handleDbMigrationApply(opts) {
|
|
108
|
+
const appId = (0, shared_1.resolveAppId)(opts);
|
|
109
|
+
// TTY 下先 diff 给用户审;--yes 直接打到 online
|
|
110
|
+
if (!opts.yes && !(0, output_1.isJsonMode)()) {
|
|
111
|
+
let preview;
|
|
112
|
+
try {
|
|
113
|
+
preview = await api.db.migrate({ appId, dryRun: true });
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
throw decorateMigrationError(err);
|
|
117
|
+
}
|
|
118
|
+
if (preview.changes.length === 0) {
|
|
119
|
+
// PRD 文案 + hint
|
|
120
|
+
throw new error_1.AppError("DB_API_k_dl_1300035", `No pending changes between ${preview.from} and ${preview.to}`, {
|
|
121
|
+
next_actions: [
|
|
122
|
+
'Make schema changes in dev first (e.g. `miaoda db sql "ALTER TABLE ..." --env dev`)',
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
renderDiff(preview);
|
|
127
|
+
const ok = await confirm(`? Apply ${String(preview.changes.length)} change(s) to ${preview.to}? (y/N) `);
|
|
128
|
+
if (!ok) {
|
|
129
|
+
(0, output_1.emit)("Aborted.");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
let result;
|
|
134
|
+
try {
|
|
135
|
+
result = await api.db.migrate({ appId, dryRun: false });
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
throw decorateMigrationError(err);
|
|
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;
|
|
162
|
+
if ((0, output_1.isJsonMode)()) {
|
|
163
|
+
// PRD:{"status": "applied", "from": "dev", "to": "online", "changes_applied": 2}
|
|
164
|
+
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
|
|
165
|
+
status: "applied",
|
|
166
|
+
from: result.from,
|
|
167
|
+
to: result.to,
|
|
168
|
+
changesApplied: appliedCount,
|
|
169
|
+
}));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
173
|
+
const prefix = tty ? "✓" : "OK";
|
|
174
|
+
const arrow = tty ? "→" : "->";
|
|
175
|
+
(0, output_1.emit)(`${prefix} Applied ${result.from} ${arrow} ${result.to} (${String(appliedCount)} changes)`);
|
|
176
|
+
}
|
|
177
|
+
// ── helpers ──
|
|
178
|
+
// PRD diff 输出:
|
|
179
|
+
// dev → online (2 changes):
|
|
180
|
+
//
|
|
181
|
+
// ALTER TABLE users ADD COLUMN avatar_url text;
|
|
182
|
+
// CREATE INDEX idx_users_avatar ON users(avatar_url);
|
|
183
|
+
function renderDiff(result) {
|
|
184
|
+
if ((0, output_1.isJsonMode)()) {
|
|
185
|
+
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
|
|
186
|
+
from: result.from,
|
|
187
|
+
to: result.to,
|
|
188
|
+
changes: result.changes.map((c) => ({
|
|
189
|
+
type: c.type,
|
|
190
|
+
table: c.table,
|
|
191
|
+
statement: c.statement,
|
|
192
|
+
})),
|
|
193
|
+
}));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
197
|
+
if (result.changes.length === 0) {
|
|
198
|
+
(0, output_1.emit)(`No pending changes from ${result.from} to ${result.to}.`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const arrow = tty ? "→" : "->";
|
|
202
|
+
(0, output_1.emit)(`${result.from} ${arrow} ${result.to} (${String(result.changes.length)} changes):\n\n` +
|
|
203
|
+
result.changes.map((c) => ` ${c.statement}`).join("\n"));
|
|
204
|
+
}
|
|
205
|
+
// decorateMigrationError 给 migration / recovery 路径上的几个错误码补 PRD 规定的 hint。
|
|
206
|
+
// dataloom 后端只返 message + code,hint 由 CLI 端按错误码映射;其它错误码原样透传。
|
|
207
|
+
function decorateMigrationError(err) {
|
|
208
|
+
if (!(err instanceof error_1.AppError))
|
|
209
|
+
return err;
|
|
210
|
+
switch (err.code) {
|
|
211
|
+
case "DB_API_k_dl_1300039":
|
|
212
|
+
// 多环境未初始化:引导先 init
|
|
213
|
+
return new error_1.AppError(err.code, err.message, {
|
|
214
|
+
next_actions: ["Run `miaoda db migration init` to set up multi-env first."],
|
|
215
|
+
});
|
|
216
|
+
case "DB_API_k_dl_1300035":
|
|
217
|
+
// 无待发布变更:引导先在 dev 改 schema
|
|
218
|
+
return new error_1.AppError(err.code, err.message, {
|
|
219
|
+
next_actions: [
|
|
220
|
+
'Make schema changes in dev first (e.g. `miaoda db sql "ALTER TABLE ..." --env dev`)',
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
default:
|
|
224
|
+
return err;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function confirm(prompt) {
|
|
228
|
+
const rl = node_readline_1.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
229
|
+
return new Promise((resolve) => {
|
|
230
|
+
rl.question(prompt, (answer) => {
|
|
231
|
+
rl.close();
|
|
232
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.handleDbQuota = handleDbQuota;
|
|
37
|
+
const api = __importStar(require("../../../api/index"));
|
|
38
|
+
const shared_1 = require("../../../cli/commands/shared");
|
|
39
|
+
const output_1 = require("../../../utils/output");
|
|
40
|
+
const render_1 = require("../../../utils/render");
|
|
41
|
+
async function handleDbQuota(opts) {
|
|
42
|
+
const appId = (0, shared_1.resolveAppId)(opts);
|
|
43
|
+
const data = await api.db.getDbQuota({ appId, dbBranch: opts.env });
|
|
44
|
+
if ((0, output_1.isJsonMode)()) {
|
|
45
|
+
// 配额未对接(storageQuotaBytes=0)时,quota / usage_percent 字段都不输出
|
|
46
|
+
const out = {
|
|
47
|
+
storageUsedBytes: data.storageUsedBytes,
|
|
48
|
+
tables: data.tables,
|
|
49
|
+
views: data.views,
|
|
50
|
+
};
|
|
51
|
+
if (data.storageQuotaBytes > 0) {
|
|
52
|
+
out.storageQuotaBytes = data.storageQuotaBytes;
|
|
53
|
+
out.usagePercent = data.usagePercent;
|
|
54
|
+
}
|
|
55
|
+
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)(out));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// PRD:单行 "Storage: 14.9 MB / 1 GB (1.5%)";配额未对接时只显示 used
|
|
59
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
60
|
+
const storageLine = data.storageQuotaBytes > 0
|
|
61
|
+
? `${(0, render_1.formatSize)(data.storageUsedBytes)} / ${(0, render_1.formatSize)(data.storageQuotaBytes)} (${data.usagePercent.toFixed(1)}%)`
|
|
62
|
+
: (0, render_1.formatSize)(data.storageUsedBytes);
|
|
63
|
+
(0, output_1.emit)((0, render_1.renderKeyValue)([
|
|
64
|
+
["Storage", storageLine],
|
|
65
|
+
["Tables", String(data.tables)],
|
|
66
|
+
["Views", String(data.views)],
|
|
67
|
+
], tty));
|
|
68
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.handleDbRecoveryDiff = handleDbRecoveryDiff;
|
|
40
|
+
exports.handleDbRecoveryApply = handleDbRecoveryApply;
|
|
41
|
+
const node_readline_1 = __importDefault(require("node:readline"));
|
|
42
|
+
const api = __importStar(require("../../../api/index"));
|
|
43
|
+
const shared_1 = require("../../../cli/commands/shared");
|
|
44
|
+
const error_1 = require("../../../utils/error");
|
|
45
|
+
const output_1 = require("../../../utils/output");
|
|
46
|
+
const poll_1 = require("../../../utils/poll");
|
|
47
|
+
const render_1 = require("../../../utils/render");
|
|
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。
|
|
53
|
+
async function handleDbRecoveryDiff(target, opts) {
|
|
54
|
+
const appId = (0, shared_1.resolveAppId)(opts);
|
|
55
|
+
const ts = normalizeTimestamp(target);
|
|
56
|
+
const preview = await runRecoveryPreview(appId, ts);
|
|
57
|
+
renderDiff(ts, preview);
|
|
58
|
+
}
|
|
59
|
+
async function handleDbRecoveryApply(target, opts) {
|
|
60
|
+
const appId = (0, shared_1.resolveAppId)(opts);
|
|
61
|
+
const ts = normalizeTimestamp(target);
|
|
62
|
+
// PITR 高危:TTY 下先 diff 给用户审;--yes 直接执行
|
|
63
|
+
if (!opts.yes && !(0, output_1.isJsonMode)()) {
|
|
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) `);
|
|
67
|
+
if (!ok) {
|
|
68
|
+
(0, output_1.emit)("Aborted.");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let result;
|
|
73
|
+
try {
|
|
74
|
+
result = await api.db.recover({ appId, target: ts, dryRun: false });
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
throw decorateRecoveryError(err);
|
|
78
|
+
}
|
|
79
|
+
if ((0, output_1.isJsonMode)()) {
|
|
80
|
+
// PRD:{"status": "restored", "target": "...", "elapsed_seconds": 30}
|
|
81
|
+
// tables_affected 来源于 preview,apply 路径不再回带,由 CLI 不强求字段。
|
|
82
|
+
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
|
|
83
|
+
status: result.status ?? "restored",
|
|
84
|
+
target: result.target,
|
|
85
|
+
elapsedSeconds: result.elapsedSeconds ?? 0,
|
|
86
|
+
}));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
90
|
+
const prefix = tty ? "✓" : "OK";
|
|
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
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// ── helpers ──
|
|
141
|
+
/**
|
|
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。
|
|
149
|
+
*/
|
|
150
|
+
function normalizeTimestamp(input) {
|
|
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";
|
|
153
|
+
if (input === "") {
|
|
154
|
+
throw new error_1.AppError("ARGS_INVALID", "target timestamp is required", {
|
|
155
|
+
next_actions: ["Usage: miaoda db recovery diff|apply <timestamp>"],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// 仅日期:YYYY-MM-DD → 按 UTC 0 点
|
|
159
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
|
|
160
|
+
const d = new Date(`${input}T00:00:00Z`);
|
|
161
|
+
if (Number.isNaN(d.getTime())) {
|
|
162
|
+
throw new error_1.AppError("INVALID_TIMESTAMP", `Invalid timestamp format '${input}'`, {
|
|
163
|
+
next_actions: [FORMAT_HINT],
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return d.toISOString();
|
|
167
|
+
}
|
|
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();
|
|
177
|
+
}
|
|
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
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// PRD diff 输出,三套:
|
|
184
|
+
//
|
|
185
|
+
// TTY pretty(缩进 prose、带 Unicode 箭头 / 波浪号、表名按列对齐):
|
|
186
|
+
// Recovery preview (→ 2026-04-15T10:00:00Z):
|
|
187
|
+
//
|
|
188
|
+
// tables affected: 2
|
|
189
|
+
// users: +3 rows, -1 row, ~5 rows modified
|
|
190
|
+
// orders: table will be restored (was dropped at 10:25:00)
|
|
191
|
+
//
|
|
192
|
+
// estimated time: ~30s
|
|
193
|
+
//
|
|
194
|
+
// non-TTY pretty(管道友好,扁平 TSV、ASCII 箭头、snake_case key、estimated_time 不带 ~/s):
|
|
195
|
+
// Recovery preview (-> 2026-04-15T10:00:00Z):
|
|
196
|
+
// tables_affected\t2
|
|
197
|
+
// users\t+3 rows, -1 row, ~5 rows modified
|
|
198
|
+
// orders\ttable will be restored (was dropped at 10:25:00)
|
|
199
|
+
// estimated_time\t30
|
|
200
|
+
//
|
|
201
|
+
// --json:标准 envelope,字段名固定 snake_case。
|
|
202
|
+
function renderDiff(target, preview) {
|
|
203
|
+
const changes = preview.changes ?? [];
|
|
204
|
+
const tablesAffected = preview.tablesAffected ?? changes.length;
|
|
205
|
+
const estimated = preview.estimatedSeconds;
|
|
206
|
+
if ((0, output_1.isJsonMode)()) {
|
|
207
|
+
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
|
|
208
|
+
target,
|
|
209
|
+
tablesAffected,
|
|
210
|
+
changes: changes.map((c) => ({
|
|
211
|
+
table: c.table,
|
|
212
|
+
inserted: c.inserted,
|
|
213
|
+
deleted: c.deleted,
|
|
214
|
+
modified: c.modified,
|
|
215
|
+
action: c.action,
|
|
216
|
+
droppedAt: c.droppedAt,
|
|
217
|
+
})),
|
|
218
|
+
estimatedSeconds: estimated ?? 0,
|
|
219
|
+
}));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
223
|
+
if (tty) {
|
|
224
|
+
(0, output_1.emit)(renderDiffTty(target, changes, tablesAffected, estimated));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
(0, output_1.emit)(renderDiffPipe(target, changes, tablesAffected, estimated));
|
|
228
|
+
}
|
|
229
|
+
function renderDiffTty(target, changes, tablesAffected, estimated) {
|
|
230
|
+
const header = `Recovery preview (→ ${target}):`;
|
|
231
|
+
if (changes.length === 0) {
|
|
232
|
+
return `${header}\n\n No changes — database is already at this state.`;
|
|
233
|
+
}
|
|
234
|
+
// 对齐:所有表名右侧补到 max(len) + 1 个 ":" + 1 个空格再起描述。PRD 用例里
|
|
235
|
+
// "users: " / "orders: " 三/二空格其实就是按 6 个 char 列宽对齐出来的。
|
|
236
|
+
const maxName = Math.max(...changes.map((c) => c.table.length));
|
|
237
|
+
const lines = [header, "", ` tables affected: ${String(tablesAffected)}`];
|
|
238
|
+
for (const c of changes) {
|
|
239
|
+
const pad = " ".repeat(Math.max(1, maxName - c.table.length + 2));
|
|
240
|
+
lines.push(` ${c.table}:${pad}${describeChange(c)}`);
|
|
241
|
+
}
|
|
242
|
+
// PRD 要求 estimated time 块固定出现(即便 dataloom 当前没填 EstimatedSeconds,
|
|
243
|
+
// CLI 也用 0 兜底,保证输出结构稳定不漂)。
|
|
244
|
+
lines.push("");
|
|
245
|
+
lines.push(` estimated time: ~${String(estimated ?? 0)}s`);
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
248
|
+
function renderDiffPipe(target, changes, tablesAffected, estimated) {
|
|
249
|
+
// 管道场景:去掉缩进,TAB 分列,箭头降级为 ->,key 用 snake_case,
|
|
250
|
+
// estimated_time 只输出秒数(不带 ~/s),方便 awk/grep。
|
|
251
|
+
const header = `Recovery preview (-> ${target}):`;
|
|
252
|
+
if (changes.length === 0) {
|
|
253
|
+
return `${header}\nNo changes — database is already at this state.`;
|
|
254
|
+
}
|
|
255
|
+
const lines = [header, `tables_affected\t${String(tablesAffected)}`];
|
|
256
|
+
for (const c of changes) {
|
|
257
|
+
lines.push(`${c.table}\t${describeChange(c)}`);
|
|
258
|
+
}
|
|
259
|
+
// 与 TTY 一致,始终输出 estimated_time(默认 0),让 awk 解析时不用兜底。
|
|
260
|
+
lines.push(`estimated_time\t${String(estimated ?? 0)}`);
|
|
261
|
+
return lines.join("\n");
|
|
262
|
+
}
|
|
263
|
+
function describeChange(c) {
|
|
264
|
+
// 表级操作:dataloom 现在把 schema diff 透出为 action="schema_<diffType>",
|
|
265
|
+
// 老协议的 restore_table / drop / create 一并保留兼容。
|
|
266
|
+
if (c.action === "restore_table") {
|
|
267
|
+
const ts = c.droppedAt ? ` (was dropped at ${c.droppedAt})` : "";
|
|
268
|
+
return `table will be restored${ts}`;
|
|
269
|
+
}
|
|
270
|
+
if (c.action === "drop") {
|
|
271
|
+
const ts = c.droppedAt ? ` (was dropped at ${c.droppedAt})` : "";
|
|
272
|
+
return `table will be dropped${ts}`;
|
|
273
|
+
}
|
|
274
|
+
if (c.action === "create") {
|
|
275
|
+
const ts = c.droppedAt ? ` (was dropped at ${c.droppedAt})` : "";
|
|
276
|
+
return `table will be created${ts}`;
|
|
277
|
+
}
|
|
278
|
+
if (c.action?.startsWith("schema_") === true) {
|
|
279
|
+
// schema diff 子类不细化,统一显示 "schema changed (<diffType>)"
|
|
280
|
+
const diffType = c.action.slice("schema_".length);
|
|
281
|
+
return `schema changed${diffType !== "" ? ` (${diffType})` : ""}`;
|
|
282
|
+
}
|
|
283
|
+
if (c.action === "unavailable") {
|
|
284
|
+
// dataloom 端 count 失败的表,复用 droppedAt 透传 message
|
|
285
|
+
return `diff unavailable${c.droppedAt !== undefined && c.droppedAt !== "" ? `: ${c.droppedAt}` : ""}`;
|
|
286
|
+
}
|
|
287
|
+
// 数据变更:+N rows / -N rows / ~N rows modified
|
|
288
|
+
const parts = [];
|
|
289
|
+
if (c.inserted !== undefined && c.inserted !== 0)
|
|
290
|
+
parts.push(`+${String(c.inserted)} rows`);
|
|
291
|
+
if (c.deleted !== undefined && c.deleted !== 0) {
|
|
292
|
+
parts.push(`-${String(c.deleted)} ${c.deleted === 1 ? "row" : "rows"}`);
|
|
293
|
+
}
|
|
294
|
+
if (c.modified !== undefined && c.modified !== 0)
|
|
295
|
+
parts.push(`~${String(c.modified)} rows modified`);
|
|
296
|
+
return parts.length === 0 ? "no changes" : parts.join(", ");
|
|
297
|
+
}
|
|
298
|
+
// decorateRecoveryError 给 recovery 路径上的几个错误码补 PRD 规定的 hint。
|
|
299
|
+
// dataloom 后端只返 message + code,hint 由 CLI 端按错误码映射;其它错误码原样透传。
|
|
300
|
+
function decorateRecoveryError(err) {
|
|
301
|
+
if (!(err instanceof error_1.AppError))
|
|
302
|
+
return err;
|
|
303
|
+
switch (err.code) {
|
|
304
|
+
case "DB_API_k_dl_1300036":
|
|
305
|
+
// 窗口超限:引导用户检查 last migration apply 时间
|
|
306
|
+
return new error_1.AppError(err.code, err.message, {
|
|
307
|
+
next_actions: [
|
|
308
|
+
"PITR window is up to 7 days back, limited by your last `db migration apply` time.",
|
|
309
|
+
],
|
|
310
|
+
});
|
|
311
|
+
case "DB_API_k_dl_1300038":
|
|
312
|
+
// 时间格式错误:引导 ISO 8601
|
|
313
|
+
return new error_1.AppError(err.code, err.message, {
|
|
314
|
+
next_actions: ["Use ISO 8601 format, e.g. 2026-04-15 or 2026-04-15T10:00:00Z"],
|
|
315
|
+
});
|
|
316
|
+
default:
|
|
317
|
+
return err;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function confirm(prompt) {
|
|
321
|
+
const rl = node_readline_1.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
322
|
+
return new Promise((resolve) => {
|
|
323
|
+
rl.question(prompt, (answer) => {
|
|
324
|
+
rl.close();
|
|
325
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
@@ -39,12 +39,11 @@ const api = __importStar(require("../../../api/index"));
|
|
|
39
39
|
const error_1 = require("../../../utils/error");
|
|
40
40
|
const output_1 = require("../../../utils/output");
|
|
41
41
|
const render_1 = require("../../../utils/render");
|
|
42
|
-
const shared_1 = require("../../../cli/commands/shared");
|
|
43
42
|
const colors_1 = require("../../../utils/colors");
|
|
44
43
|
const index_1 = require("../../../api/db/index");
|
|
45
44
|
// ── schema list ──
|
|
46
45
|
async function handleDbSchemaList(opts) {
|
|
47
|
-
const appId =
|
|
46
|
+
const appId = opts.appId;
|
|
48
47
|
const resp = await api.db.getSchema({
|
|
49
48
|
appId,
|
|
50
49
|
format: "schema",
|
|
@@ -80,7 +79,7 @@ async function handleDbSchemaList(opts) {
|
|
|
80
79
|
(0, output_1.emit)(tty ? (0, render_1.renderAlignedTable)(headers, rows) : (0, render_1.renderTsv)(headers, rows));
|
|
81
80
|
}
|
|
82
81
|
async function handleDbSchemaGet(table, opts) {
|
|
83
|
-
const appId =
|
|
82
|
+
const appId = opts.appId;
|
|
84
83
|
const tty = (0, render_1.isStdoutTty)();
|
|
85
84
|
const forceDdl = Boolean(opts.ddl);
|
|
86
85
|
const wantsStructured = (0, output_1.isJsonMode)() || (tty && !forceDdl);
|
|
@@ -42,7 +42,6 @@ const error_1 = require("../../../utils/error");
|
|
|
42
42
|
const output_1 = require("../../../utils/output");
|
|
43
43
|
const config_1 = require("../../../utils/config");
|
|
44
44
|
const logger_1 = require("../../../utils/logger");
|
|
45
|
-
const shared_1 = require("../../../cli/commands/shared");
|
|
46
45
|
const render_1 = require("../../../utils/render");
|
|
47
46
|
const colors_1 = require("../../../utils/colors");
|
|
48
47
|
const fuzzy_match_1 = require("../../../utils/fuzzy-match");
|
|
@@ -62,7 +61,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
62
61
|
* 末尾汇总,--json 输出 data 数组。
|
|
63
62
|
*/
|
|
64
63
|
async function handleDbSql(query, opts) {
|
|
65
|
-
const appId =
|
|
64
|
+
const appId = opts.appId;
|
|
66
65
|
const sql = await readSql(query);
|
|
67
66
|
if (!sql.trim()) {
|
|
68
67
|
throw new error_1.AppError("ARGS_INVALID", "Empty SQL (no inline query and stdin is empty)");
|