@lark-apaas/miaoda-cli 0.1.2-alpha.5fec91b → 0.1.2-alpha.6fcfeeb
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 +317 -6
- package/dist/api/db/client.js +36 -0
- package/dist/api/db/index.js +11 -1
- package/dist/api/file/api.js +15 -0
- package/dist/api/file/index.js +2 -1
- package/dist/api/index.js +1 -7
- package/dist/cli/commands/db/index.js +137 -0
- package/dist/cli/commands/file/index.js +7 -0
- package/dist/cli/commands/index.js +0 -6
- package/dist/cli/commands/shared.js +3 -61
- 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 +1 -1
- package/dist/cli/handlers/db/index.js +17 -1
- package/dist/cli/handlers/db/migration.js +233 -0
- package/dist/cli/handlers/{app/update.js → db/quota.js} +29 -20
- package/dist/cli/handlers/db/recovery.js +277 -0
- package/dist/cli/handlers/file/index.js +3 -1
- package/dist/cli/handlers/{deploy/error-log.js → file/quota.js} +27 -22
- package/dist/utils/http.js +0 -118
- package/dist/utils/index.js +3 -13
- package/dist/utils/output.js +29 -338
- package/dist/utils/poll.js +27 -0
- package/package.json +5 -7
- package/dist/api/app/api.js +0 -25
- package/dist/api/app/index.js +0 -15
- package/dist/api/app/schemas.js +0 -79
- package/dist/api/app/types.js +0 -58
- package/dist/api/deploy/api.js +0 -60
- package/dist/api/deploy/index.js +0 -16
- package/dist/api/deploy/schemas.js +0 -103
- package/dist/api/deploy/types.js +0 -22
- package/dist/api/observability/api.js +0 -52
- package/dist/api/observability/index.js +0 -16
- package/dist/api/observability/schemas.js +0 -39
- package/dist/api/observability/types.js +0 -27
- package/dist/cli/commands/app/index.js +0 -62
- package/dist/cli/commands/deploy/index.js +0 -140
- package/dist/cli/commands/observability/index.js +0 -229
- package/dist/cli/handlers/app/get.js +0 -48
- package/dist/cli/handlers/app/index.js +0 -7
- package/dist/cli/handlers/deploy/deploy.js +0 -83
- package/dist/cli/handlers/deploy/get.js +0 -70
- package/dist/cli/handlers/deploy/helpers.js +0 -41
- package/dist/cli/handlers/deploy/history.js +0 -70
- package/dist/cli/handlers/deploy/index.js +0 -14
- package/dist/cli/handlers/deploy/polling.js +0 -139
- package/dist/cli/handlers/observability/analytics.js +0 -190
- package/dist/cli/handlers/observability/helpers.js +0 -66
- package/dist/cli/handlers/observability/index.js +0 -12
- package/dist/cli/handlers/observability/log.js +0 -94
- package/dist/cli/handlers/observability/metric.js +0 -208
- package/dist/cli/handlers/observability/trace.js +0 -102
- package/dist/utils/devops-error.js +0 -28
- package/dist/utils/git.js +0 -29
- package/dist/utils/time.js +0 -132
|
@@ -0,0 +1,233 @@
|
|
|
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
|
+
if (cur.status === "success")
|
|
152
|
+
return { done: true, value: cur };
|
|
153
|
+
if (cur.status === "failed") {
|
|
154
|
+
throw new error_1.AppError("DB_API_k_dl_1300030", cur.errorMessage ?? `migration apply failed (taskId=${taskId})`);
|
|
155
|
+
}
|
|
156
|
+
return { done: false };
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
const appliedCount = final.changesApplied ?? result.changes.length;
|
|
160
|
+
if ((0, output_1.isJsonMode)()) {
|
|
161
|
+
// PRD:{"status": "applied", "from": "dev", "to": "online", "changes_applied": 2}
|
|
162
|
+
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
|
|
163
|
+
status: "applied",
|
|
164
|
+
from: result.from,
|
|
165
|
+
to: result.to,
|
|
166
|
+
changesApplied: appliedCount,
|
|
167
|
+
}));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
171
|
+
const prefix = tty ? "✓" : "OK";
|
|
172
|
+
const arrow = tty ? "→" : "->";
|
|
173
|
+
(0, output_1.emit)(`${prefix} Applied ${result.from} ${arrow} ${result.to} (${String(appliedCount)} changes)`);
|
|
174
|
+
}
|
|
175
|
+
// ── helpers ──
|
|
176
|
+
// PRD diff 输出:
|
|
177
|
+
// dev → online (2 changes):
|
|
178
|
+
//
|
|
179
|
+
// ALTER TABLE users ADD COLUMN avatar_url text;
|
|
180
|
+
// CREATE INDEX idx_users_avatar ON users(avatar_url);
|
|
181
|
+
function renderDiff(result) {
|
|
182
|
+
if ((0, output_1.isJsonMode)()) {
|
|
183
|
+
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
|
|
184
|
+
from: result.from,
|
|
185
|
+
to: result.to,
|
|
186
|
+
changes: result.changes.map((c) => ({
|
|
187
|
+
type: c.type,
|
|
188
|
+
table: c.table,
|
|
189
|
+
statement: c.statement,
|
|
190
|
+
})),
|
|
191
|
+
}));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
195
|
+
if (result.changes.length === 0) {
|
|
196
|
+
(0, output_1.emit)(`No pending changes from ${result.from} to ${result.to}.`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const arrow = tty ? "→" : "->";
|
|
200
|
+
(0, output_1.emit)(`${result.from} ${arrow} ${result.to} (${String(result.changes.length)} changes):\n\n` +
|
|
201
|
+
result.changes.map((c) => ` ${c.statement}`).join("\n"));
|
|
202
|
+
}
|
|
203
|
+
// decorateMigrationError 给 migration / recovery 路径上的几个错误码补 PRD 规定的 hint。
|
|
204
|
+
// dataloom 后端只返 message + code,hint 由 CLI 端按错误码映射;其它错误码原样透传。
|
|
205
|
+
function decorateMigrationError(err) {
|
|
206
|
+
if (!(err instanceof error_1.AppError))
|
|
207
|
+
return err;
|
|
208
|
+
switch (err.code) {
|
|
209
|
+
case "DB_API_k_dl_1300039":
|
|
210
|
+
// 多环境未初始化:引导先 init
|
|
211
|
+
return new error_1.AppError(err.code, err.message, {
|
|
212
|
+
next_actions: ["Run `miaoda db migration init` to set up multi-env first."],
|
|
213
|
+
});
|
|
214
|
+
case "DB_API_k_dl_1300035":
|
|
215
|
+
// 无待发布变更:引导先在 dev 改 schema
|
|
216
|
+
return new error_1.AppError(err.code, err.message, {
|
|
217
|
+
next_actions: [
|
|
218
|
+
'Make schema changes in dev first (e.g. `miaoda db sql "ALTER TABLE ..." --env dev`)',
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
default:
|
|
222
|
+
return err;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function confirm(prompt) {
|
|
226
|
+
const rl = node_readline_1.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
227
|
+
return new Promise((resolve) => {
|
|
228
|
+
rl.question(prompt, (answer) => {
|
|
229
|
+
rl.close();
|
|
230
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
@@ -33,27 +33,36 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
36
|
+
exports.handleDbQuota = handleDbQuota;
|
|
37
37
|
const api = __importStar(require("../../../api/index"));
|
|
38
|
-
const output_1 = require("../../../utils/output");
|
|
39
38
|
const shared_1 = require("../../../cli/commands/shared");
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
async function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
57
|
}
|
|
58
|
-
|
|
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));
|
|
59
68
|
}
|
|
@@ -0,0 +1,277 @@
|
|
|
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
|
+
if (cur.previewStatus === "success")
|
|
122
|
+
return { done: true, value: cur };
|
|
123
|
+
if (cur.previewStatus === "failed") {
|
|
124
|
+
// 复用 k_dl_1300036(窗口超限 / 预览失败的统一对外码),让 decorateRecoveryError
|
|
125
|
+
// 命中后给 PITR 7 天窗口的 hint。errorMessage 优先用 dataloom 翻好的 PRD 文案。
|
|
126
|
+
throw new error_1.AppError("DB_API_k_dl_1300036", cur.errorMessage ?? "recovery preview failed");
|
|
127
|
+
}
|
|
128
|
+
return { done: false };
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
throw decorateRecoveryError(err);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ── helpers ──
|
|
137
|
+
/**
|
|
138
|
+
* 把用户传入的时间统一成 ISO 8601 UTC。PRD 只接受两种格式:
|
|
139
|
+
* - `YYYY-MM-DD`(按 UTC 0 点解释)
|
|
140
|
+
* - 完整 ISO 8601 `YYYY-MM-DDTHH:MM:SS[Z|±HH:MM]`
|
|
141
|
+
*
|
|
142
|
+
* 严格 regex 匹配 + 不用 new Date(input) 兜底——Node Date 解析 `2026/04/15` 这种
|
|
143
|
+
* 非 ISO 字符串居然成功(落到本地时区),绕过格式校验把脏值送到后端,导致用户拿到
|
|
144
|
+
* 「窗口超限」错误而不是「格式错误」。PRD 明确这两种格式以外的都应该报 INVALID_TIMESTAMP。
|
|
145
|
+
*/
|
|
146
|
+
function normalizeTimestamp(input) {
|
|
147
|
+
// PRD hint 文案对齐 PRD 截图(同时是 dataloom 端 ErrInvalidTimestamp 的 hint)
|
|
148
|
+
const FORMAT_HINT = "Use ISO 8601 format, e.g. 2026-04-15 or 2026-04-15T10:00:00Z";
|
|
149
|
+
if (input === "") {
|
|
150
|
+
throw new error_1.AppError("ARGS_INVALID", "target timestamp is required", {
|
|
151
|
+
next_actions: ["Usage: miaoda db recovery diff|apply <timestamp>"],
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// 仅日期:YYYY-MM-DD → 按 UTC 0 点
|
|
155
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
|
|
156
|
+
const d = new Date(`${input}T00:00:00Z`);
|
|
157
|
+
if (Number.isNaN(d.getTime())) {
|
|
158
|
+
throw new error_1.AppError("INVALID_TIMESTAMP", `Invalid timestamp format '${input}'`, {
|
|
159
|
+
next_actions: [FORMAT_HINT],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return d.toISOString();
|
|
163
|
+
}
|
|
164
|
+
// 完整 ISO 8601 datetime:T 分隔,秒可选 ms,Z 或 ±HH:MM 时区
|
|
165
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/.test(input)) {
|
|
166
|
+
const d = new Date(input);
|
|
167
|
+
if (Number.isNaN(d.getTime())) {
|
|
168
|
+
throw new error_1.AppError("INVALID_TIMESTAMP", `Invalid timestamp format '${input}'`, {
|
|
169
|
+
next_actions: [FORMAT_HINT],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return d.toISOString();
|
|
173
|
+
}
|
|
174
|
+
// 其它一律按格式错误拒绝(`2026/04/15` / `2026.04.15` / `2026年4月15日` 等都走这里)
|
|
175
|
+
throw new error_1.AppError("INVALID_TIMESTAMP", `Invalid timestamp format '${input}'`, {
|
|
176
|
+
next_actions: [FORMAT_HINT],
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// PRD diff 输出(结构化 prose):
|
|
180
|
+
// Recovery preview (→ 2026-04-15T10:00:00Z):
|
|
181
|
+
//
|
|
182
|
+
// tables affected: 2
|
|
183
|
+
// users: +3 rows, -1 row, ~5 rows modified
|
|
184
|
+
// orders: table will be restored (was dropped at 10:25:00)
|
|
185
|
+
//
|
|
186
|
+
// estimated time: ~30s
|
|
187
|
+
function renderDiff(target, preview) {
|
|
188
|
+
const changes = preview.changes ?? [];
|
|
189
|
+
const tablesAffected = preview.tablesAffected ?? changes.length;
|
|
190
|
+
const estimated = preview.estimatedSeconds;
|
|
191
|
+
if ((0, output_1.isJsonMode)()) {
|
|
192
|
+
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)({
|
|
193
|
+
target,
|
|
194
|
+
tablesAffected,
|
|
195
|
+
changes: changes.map((c) => ({
|
|
196
|
+
table: c.table,
|
|
197
|
+
inserted: c.inserted,
|
|
198
|
+
deleted: c.deleted,
|
|
199
|
+
modified: c.modified,
|
|
200
|
+
action: c.action,
|
|
201
|
+
droppedAt: c.droppedAt,
|
|
202
|
+
})),
|
|
203
|
+
estimatedSeconds: estimated ?? 0,
|
|
204
|
+
}));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
208
|
+
const arrow = tty ? "→" : "->";
|
|
209
|
+
if (changes.length === 0) {
|
|
210
|
+
(0, output_1.emit)(`Recovery preview (${arrow} ${target}):\n\n` +
|
|
211
|
+
` No changes — database is already at this state.`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const lines = [
|
|
215
|
+
`Recovery preview (${arrow} ${target}):`,
|
|
216
|
+
"",
|
|
217
|
+
` tables affected: ${String(tablesAffected)}`,
|
|
218
|
+
];
|
|
219
|
+
for (const c of changes) {
|
|
220
|
+
lines.push(` ${c.table}: ${describeChange(c)}`);
|
|
221
|
+
}
|
|
222
|
+
if (estimated !== undefined) {
|
|
223
|
+
lines.push("");
|
|
224
|
+
lines.push(` estimated time: ~${String(estimated)}s`);
|
|
225
|
+
}
|
|
226
|
+
(0, output_1.emit)(lines.join("\n"));
|
|
227
|
+
}
|
|
228
|
+
function describeChange(c) {
|
|
229
|
+
if (c.action === "restore_table" || c.action === "drop" || c.action === "create") {
|
|
230
|
+
const ts = c.droppedAt ? ` (was dropped at ${c.droppedAt})` : "";
|
|
231
|
+
if (c.action === "restore_table")
|
|
232
|
+
return `table will be restored${ts}`;
|
|
233
|
+
if (c.action === "drop")
|
|
234
|
+
return `table will be dropped${ts}`;
|
|
235
|
+
return `table will be created${ts}`;
|
|
236
|
+
}
|
|
237
|
+
const parts = [];
|
|
238
|
+
if (c.inserted !== undefined && c.inserted !== 0)
|
|
239
|
+
parts.push(`+${String(c.inserted)} rows`);
|
|
240
|
+
if (c.deleted !== undefined && c.deleted !== 0) {
|
|
241
|
+
parts.push(`-${String(c.deleted)} ${c.deleted === 1 ? "row" : "rows"}`);
|
|
242
|
+
}
|
|
243
|
+
if (c.modified !== undefined && c.modified !== 0)
|
|
244
|
+
parts.push(`~${String(c.modified)} rows modified`);
|
|
245
|
+
return parts.length === 0 ? "no changes" : parts.join(", ");
|
|
246
|
+
}
|
|
247
|
+
// decorateRecoveryError 给 recovery 路径上的几个错误码补 PRD 规定的 hint。
|
|
248
|
+
// dataloom 后端只返 message + code,hint 由 CLI 端按错误码映射;其它错误码原样透传。
|
|
249
|
+
function decorateRecoveryError(err) {
|
|
250
|
+
if (!(err instanceof error_1.AppError))
|
|
251
|
+
return err;
|
|
252
|
+
switch (err.code) {
|
|
253
|
+
case "DB_API_k_dl_1300036":
|
|
254
|
+
// 窗口超限:引导用户检查 last migration apply 时间
|
|
255
|
+
return new error_1.AppError(err.code, err.message, {
|
|
256
|
+
next_actions: [
|
|
257
|
+
"PITR window is up to 7 days back, limited by your last `db migration apply` time.",
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
case "DB_API_k_dl_1300038":
|
|
261
|
+
// 时间格式错误:引导 ISO 8601
|
|
262
|
+
return new error_1.AppError(err.code, err.message, {
|
|
263
|
+
next_actions: ["Use ISO 8601 format, e.g. 2026-04-15 or 2026-04-15T10:00:00Z"],
|
|
264
|
+
});
|
|
265
|
+
default:
|
|
266
|
+
return err;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function confirm(prompt) {
|
|
270
|
+
const rl = node_readline_1.default.createInterface({ input: process.stdin, output: process.stderr });
|
|
271
|
+
return new Promise((resolve) => {
|
|
272
|
+
rl.question(prompt, (answer) => {
|
|
273
|
+
rl.close();
|
|
274
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.handleFileSign = exports.handleFileRm = exports.handleFileCp = exports.handleFileStat = exports.handleFileLs = void 0;
|
|
3
|
+
exports.handleFileQuota = exports.handleFileSign = exports.handleFileRm = exports.handleFileCp = exports.handleFileStat = exports.handleFileLs = void 0;
|
|
4
4
|
var ls_1 = require("./ls");
|
|
5
5
|
Object.defineProperty(exports, "handleFileLs", { enumerable: true, get: function () { return ls_1.handleFileLs; } });
|
|
6
6
|
var stat_1 = require("./stat");
|
|
@@ -11,3 +11,5 @@ var rm_1 = require("./rm");
|
|
|
11
11
|
Object.defineProperty(exports, "handleFileRm", { enumerable: true, get: function () { return rm_1.handleFileRm; } });
|
|
12
12
|
var sign_1 = require("./sign");
|
|
13
13
|
Object.defineProperty(exports, "handleFileSign", { enumerable: true, get: function () { return sign_1.handleFileSign; } });
|
|
14
|
+
var quota_1 = require("./quota");
|
|
15
|
+
Object.defineProperty(exports, "handleFileQuota", { enumerable: true, get: function () { return quota_1.handleFileQuota; } });
|
|
@@ -33,29 +33,34 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
36
|
+
exports.handleFileQuota = handleFileQuota;
|
|
37
37
|
const api = __importStar(require("../../../api/index"));
|
|
38
|
-
const output_1 = require("../../../utils/output");
|
|
39
38
|
const shared_1 = require("../../../cli/commands/shared");
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// pretty 模式额外打一行整体状态(status 是 NodeStatus 数字)
|
|
57
|
-
if (!process.stdout.isTTY)
|
|
39
|
+
const output_1 = require("../../../utils/output");
|
|
40
|
+
const render_1 = require("../../../utils/render");
|
|
41
|
+
async function handleFileQuota(opts) {
|
|
42
|
+
const appId = (0, shared_1.resolveAppId)(opts);
|
|
43
|
+
const data = await api.file.getStorageQuota({ appId });
|
|
44
|
+
if ((0, output_1.isJsonMode)()) {
|
|
45
|
+
// 配额未对接(storageQuotaBytes=0)时,quota / usage_percent 字段都不输出
|
|
46
|
+
const out = {
|
|
47
|
+
storageUsedBytes: data.storageUsedBytes,
|
|
48
|
+
files: data.files,
|
|
49
|
+
};
|
|
50
|
+
if (data.storageQuotaBytes > 0) {
|
|
51
|
+
out.storageQuotaBytes = data.storageQuotaBytes;
|
|
52
|
+
out.usagePercent = data.usagePercent;
|
|
53
|
+
}
|
|
54
|
+
(0, output_1.emitOk)((0, output_1.snakeCaseKeys)(out));
|
|
58
55
|
return;
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
}
|
|
57
|
+
// PRD:单行 "Storage: 150 MB / 1 GB (15%)";配额未对接时只显示 used
|
|
58
|
+
const tty = (0, render_1.isStdoutTty)();
|
|
59
|
+
const storageLine = data.storageQuotaBytes > 0
|
|
60
|
+
? `${(0, render_1.formatSize)(data.storageUsedBytes)} / ${(0, render_1.formatSize)(data.storageQuotaBytes)} (${data.usagePercent.toFixed(1)}%)`
|
|
61
|
+
: (0, render_1.formatSize)(data.storageUsedBytes);
|
|
62
|
+
(0, output_1.emit)((0, render_1.renderKeyValue)([
|
|
63
|
+
["Storage", storageLine],
|
|
64
|
+
["Files", String(data.files)],
|
|
65
|
+
], tty));
|
|
61
66
|
}
|