metabase-cli 0.2.2
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/LICENSE +21 -0
- package/README.md +291 -0
- package/dist/index.d.mts +408 -0
- package/dist/index.d.ts +408 -0
- package/dist/index.js +735 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +675 -0
- package/dist/index.mjs.map +1 -0
- package/dist/metabase.js +1710 -0
- package/package.json +47 -0
package/dist/metabase.js
ADDED
|
@@ -0,0 +1,1710 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// bin/metabase.ts
|
|
27
|
+
var import_node_fs3 = require("fs");
|
|
28
|
+
var import_node_path3 = require("path");
|
|
29
|
+
var import_commander9 = require("commander");
|
|
30
|
+
|
|
31
|
+
// src/commands/profile.ts
|
|
32
|
+
var import_commander = require("commander");
|
|
33
|
+
|
|
34
|
+
// src/config/store.ts
|
|
35
|
+
var fs = __toESM(require("fs"));
|
|
36
|
+
var path = __toESM(require("path"));
|
|
37
|
+
var os = __toESM(require("os"));
|
|
38
|
+
function getConfigDir() {
|
|
39
|
+
return path.join(os.homedir(), ".metabase-cli");
|
|
40
|
+
}
|
|
41
|
+
function getConfigFile() {
|
|
42
|
+
return path.join(getConfigDir(), "config.json");
|
|
43
|
+
}
|
|
44
|
+
function ensureConfigDir() {
|
|
45
|
+
if (!fs.existsSync(getConfigDir())) {
|
|
46
|
+
fs.mkdirSync(getConfigDir(), { recursive: true, mode: 448 });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function defaultConfig() {
|
|
50
|
+
return { activeProfile: "", profiles: {} };
|
|
51
|
+
}
|
|
52
|
+
function loadConfig() {
|
|
53
|
+
ensureConfigDir();
|
|
54
|
+
if (!fs.existsSync(getConfigFile())) {
|
|
55
|
+
return defaultConfig();
|
|
56
|
+
}
|
|
57
|
+
const raw = fs.readFileSync(getConfigFile(), "utf-8");
|
|
58
|
+
return JSON.parse(raw);
|
|
59
|
+
}
|
|
60
|
+
function saveConfig(config) {
|
|
61
|
+
ensureConfigDir();
|
|
62
|
+
fs.writeFileSync(getConfigFile(), JSON.stringify(config, null, 2), {
|
|
63
|
+
encoding: "utf-8",
|
|
64
|
+
mode: 384
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function getActiveProfile() {
|
|
68
|
+
const config = loadConfig();
|
|
69
|
+
if (!config.activeProfile || !config.profiles[config.activeProfile]) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return config.profiles[config.activeProfile];
|
|
73
|
+
}
|
|
74
|
+
function setActiveProfile(name) {
|
|
75
|
+
const config = loadConfig();
|
|
76
|
+
if (!config.profiles[name]) {
|
|
77
|
+
throw new Error(`Profile "${name}" does not exist`);
|
|
78
|
+
}
|
|
79
|
+
config.activeProfile = name;
|
|
80
|
+
saveConfig(config);
|
|
81
|
+
}
|
|
82
|
+
function addProfile(profile) {
|
|
83
|
+
const config = loadConfig();
|
|
84
|
+
config.profiles[profile.name] = profile;
|
|
85
|
+
if (!config.activeProfile) {
|
|
86
|
+
config.activeProfile = profile.name;
|
|
87
|
+
}
|
|
88
|
+
saveConfig(config);
|
|
89
|
+
}
|
|
90
|
+
function removeProfile(name) {
|
|
91
|
+
const config = loadConfig();
|
|
92
|
+
if (!config.profiles[name]) {
|
|
93
|
+
throw new Error(`Profile "${name}" does not exist`);
|
|
94
|
+
}
|
|
95
|
+
delete config.profiles[name];
|
|
96
|
+
if (config.activeProfile === name) {
|
|
97
|
+
const remaining = Object.keys(config.profiles);
|
|
98
|
+
config.activeProfile = remaining.length > 0 ? remaining[0] : "";
|
|
99
|
+
}
|
|
100
|
+
saveConfig(config);
|
|
101
|
+
}
|
|
102
|
+
function updateProfile(name, updates) {
|
|
103
|
+
const config = loadConfig();
|
|
104
|
+
if (!config.profiles[name]) {
|
|
105
|
+
throw new Error(`Profile "${name}" does not exist`);
|
|
106
|
+
}
|
|
107
|
+
config.profiles[name] = { ...config.profiles[name], ...updates };
|
|
108
|
+
saveConfig(config);
|
|
109
|
+
}
|
|
110
|
+
function listProfiles() {
|
|
111
|
+
const config = loadConfig();
|
|
112
|
+
return {
|
|
113
|
+
profiles: Object.values(config.profiles),
|
|
114
|
+
active: config.activeProfile
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/utils/output.ts
|
|
119
|
+
var import_cli_table3 = __toESM(require("cli-table3"));
|
|
120
|
+
function formatDatasetResponse(dataset, format = "table", columns) {
|
|
121
|
+
const cols = dataset.data.cols;
|
|
122
|
+
const rows = dataset.data.rows;
|
|
123
|
+
let colIndices;
|
|
124
|
+
if (columns?.length) {
|
|
125
|
+
colIndices = columns.map((name) => {
|
|
126
|
+
const idx = cols.findIndex(
|
|
127
|
+
(c) => c.name === name || c.display_name === name
|
|
128
|
+
);
|
|
129
|
+
if (idx === -1) throw new Error(`Column "${name}" not found`);
|
|
130
|
+
return idx;
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
colIndices = cols.map((_, i) => i);
|
|
134
|
+
}
|
|
135
|
+
const filteredCols = colIndices.map((i) => cols[i]);
|
|
136
|
+
const filteredRows = rows.map((row) => colIndices.map((i) => row[i]));
|
|
137
|
+
switch (format) {
|
|
138
|
+
case "json":
|
|
139
|
+
return JSON.stringify(
|
|
140
|
+
filteredRows.map(
|
|
141
|
+
(row) => Object.fromEntries(
|
|
142
|
+
filteredCols.map((col, i) => [col.name, row[i]])
|
|
143
|
+
)
|
|
144
|
+
),
|
|
145
|
+
null,
|
|
146
|
+
2
|
|
147
|
+
);
|
|
148
|
+
case "csv":
|
|
149
|
+
return formatDelimited(filteredCols, filteredRows, ",");
|
|
150
|
+
case "tsv":
|
|
151
|
+
return formatDelimited(filteredCols, filteredRows, " ");
|
|
152
|
+
case "table":
|
|
153
|
+
default:
|
|
154
|
+
return formatTable(filteredCols, filteredRows);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function formatDelimited(cols, rows, delimiter) {
|
|
158
|
+
const header = cols.map((c) => escapeCsvField(String(c.name), delimiter)).join(delimiter);
|
|
159
|
+
const body = rows.map(
|
|
160
|
+
(row) => row.map((cell) => escapeCsvField(formatCell(cell), delimiter)).join(delimiter)
|
|
161
|
+
);
|
|
162
|
+
return [header, ...body].join("\n");
|
|
163
|
+
}
|
|
164
|
+
function escapeCsvField(value, delimiter) {
|
|
165
|
+
if (value.includes(delimiter) || value.includes('"') || value.includes("\n")) {
|
|
166
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
167
|
+
}
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
function formatTable(cols, rows) {
|
|
171
|
+
const table = new import_cli_table3.default({
|
|
172
|
+
head: cols.map((c) => c.display_name),
|
|
173
|
+
style: { head: ["cyan"] }
|
|
174
|
+
});
|
|
175
|
+
for (const row of rows) {
|
|
176
|
+
table.push(row.map((cell) => formatCell(cell)));
|
|
177
|
+
}
|
|
178
|
+
return table.toString();
|
|
179
|
+
}
|
|
180
|
+
function formatCell(value) {
|
|
181
|
+
if (value === null || value === void 0) return "";
|
|
182
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
183
|
+
return String(value);
|
|
184
|
+
}
|
|
185
|
+
function formatJson(data) {
|
|
186
|
+
return JSON.stringify(data, null, 2);
|
|
187
|
+
}
|
|
188
|
+
function formatEntityTable(items, columns) {
|
|
189
|
+
const table = new import_cli_table3.default({
|
|
190
|
+
head: columns.map((c) => c.header),
|
|
191
|
+
style: { head: ["cyan"] }
|
|
192
|
+
});
|
|
193
|
+
for (const item of items) {
|
|
194
|
+
table.push(columns.map((c) => formatCell(item[c.key])));
|
|
195
|
+
}
|
|
196
|
+
return table.toString();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/commands/profile.ts
|
|
200
|
+
function profileCommand() {
|
|
201
|
+
const cmd = new import_commander.Command("profile").description("Manage Metabase profiles").addHelpText("after", `
|
|
202
|
+
The first profile added becomes the default (active) profile.
|
|
203
|
+
All commands use the active profile unless you switch with 'profile switch'.
|
|
204
|
+
|
|
205
|
+
Examples:
|
|
206
|
+
$ metabase-cli profile add prod --domain https://metabase.example.com --email you@co.com --password secret
|
|
207
|
+
$ metabase-cli profile add staging --domain https://staging.metabase.co --api-key mb_xxxxx
|
|
208
|
+
$ metabase-cli profile list
|
|
209
|
+
$ metabase-cli profile switch staging
|
|
210
|
+
$ metabase-cli profile current
|
|
211
|
+
$ metabase-cli profile remove staging`);
|
|
212
|
+
cmd.command("add <name>").description("Add a new profile (becomes default if first)").requiredOption("--domain <url>", "Metabase instance URL").option("--email <email>", "Login email").option("--password <password>", "Login password").option("--api-key <key>", "API key (alternative to email/password)").option("--default-db <id>", "Default database ID for queries", parseInt).addHelpText("after", `
|
|
213
|
+
Examples:
|
|
214
|
+
$ metabase-cli profile add prod --domain https://metabase.example.com --email you@co.com --password secret
|
|
215
|
+
$ metabase-cli profile add prod --domain https://metabase.example.com --email you@co.com --password secret --default-db 1
|
|
216
|
+
$ metabase-cli profile add staging --domain https://staging.example.com --api-key mb_xxxxx`).action((name, opts) => {
|
|
217
|
+
let auth;
|
|
218
|
+
if (opts.apiKey) {
|
|
219
|
+
auth = { method: "api-key", apiKey: opts.apiKey };
|
|
220
|
+
} else if (opts.email && opts.password) {
|
|
221
|
+
auth = { method: "session", email: opts.email, password: opts.password };
|
|
222
|
+
} else {
|
|
223
|
+
console.error("Error: Provide --email and --password, or --api-key");
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
const profile = {
|
|
227
|
+
name,
|
|
228
|
+
domain: opts.domain,
|
|
229
|
+
auth,
|
|
230
|
+
defaultDb: opts.defaultDb
|
|
231
|
+
};
|
|
232
|
+
addProfile(profile);
|
|
233
|
+
console.log(`Profile "${name}" added and set as active.`);
|
|
234
|
+
});
|
|
235
|
+
cmd.command("list").description("List all profiles (* = active/default)").addHelpText("after", `
|
|
236
|
+
Examples:
|
|
237
|
+
$ metabase-cli profile list`).action(() => {
|
|
238
|
+
const { profiles, active } = listProfiles();
|
|
239
|
+
if (profiles.length === 0) {
|
|
240
|
+
console.log("No profiles configured. Run: metabase-cli profile add <name>");
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const items = profiles.map((p) => ({
|
|
244
|
+
active: p.name === active ? "*" : "",
|
|
245
|
+
name: p.name,
|
|
246
|
+
domain: p.domain,
|
|
247
|
+
auth: p.auth.method,
|
|
248
|
+
user: p.user ? `${p.user.first_name} ${p.user.last_name} (${p.user.email})` : "not logged in"
|
|
249
|
+
}));
|
|
250
|
+
console.log(
|
|
251
|
+
formatEntityTable(items, [
|
|
252
|
+
{ key: "active", header: "" },
|
|
253
|
+
{ key: "name", header: "Name" },
|
|
254
|
+
{ key: "domain", header: "Domain" },
|
|
255
|
+
{ key: "auth", header: "Auth" },
|
|
256
|
+
{ key: "user", header: "User" }
|
|
257
|
+
])
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
cmd.command("switch <name>").description("Switch active (default) profile").addHelpText("after", `
|
|
261
|
+
Examples:
|
|
262
|
+
$ metabase-cli profile switch staging`).action((name) => {
|
|
263
|
+
setActiveProfile(name);
|
|
264
|
+
console.log(`Switched to profile "${name}".`);
|
|
265
|
+
});
|
|
266
|
+
cmd.command("remove <name>").description("Remove a profile").addHelpText("after", `
|
|
267
|
+
Examples:
|
|
268
|
+
$ metabase-cli profile remove staging`).action((name) => {
|
|
269
|
+
removeProfile(name);
|
|
270
|
+
console.log(`Profile "${name}" removed.`);
|
|
271
|
+
});
|
|
272
|
+
cmd.command("current").description("Show current active (default) profile").addHelpText("after", `
|
|
273
|
+
Examples:
|
|
274
|
+
$ metabase-cli profile current`).action(() => {
|
|
275
|
+
const profile = getActiveProfile();
|
|
276
|
+
if (!profile) {
|
|
277
|
+
console.log("No active profile. Run: metabase-cli profile add <name>");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
console.log(`Profile: ${profile.name}`);
|
|
281
|
+
console.log(`Domain: ${profile.domain}`);
|
|
282
|
+
console.log(`Auth: ${profile.auth.method}`);
|
|
283
|
+
if (profile.user) {
|
|
284
|
+
console.log(`User: ${profile.user.first_name} ${profile.user.last_name} (${profile.user.email})`);
|
|
285
|
+
}
|
|
286
|
+
if (profile.defaultDb) {
|
|
287
|
+
console.log(`Default DB: ${profile.defaultDb}`);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
cmd.command("set-default-db <id>").description("Set the default database ID for the active profile").addHelpText("after", `
|
|
291
|
+
The default database is used when --db is not specified in query/question commands.
|
|
292
|
+
|
|
293
|
+
Examples:
|
|
294
|
+
$ metabase-cli profile set-default-db 1
|
|
295
|
+
$ metabase-cli profile set-default-db 3`).action((id) => {
|
|
296
|
+
const profile = getActiveProfile();
|
|
297
|
+
if (!profile) {
|
|
298
|
+
console.error("No active profile. Run: metabase-cli profile add <name>");
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
updateProfile(profile.name, { defaultDb: parseInt(id) });
|
|
302
|
+
console.log(`Default database set to #${id} for profile "${profile.name}".`);
|
|
303
|
+
});
|
|
304
|
+
return cmd;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/commands/query.ts
|
|
308
|
+
var import_commander2 = require("commander");
|
|
309
|
+
var import_node_fs = require("fs");
|
|
310
|
+
var import_node_path = require("path");
|
|
311
|
+
|
|
312
|
+
// src/utils/export.ts
|
|
313
|
+
function checkExportError(buf, format) {
|
|
314
|
+
if (format !== "json" && buf.length > 0 && buf[0] === 123) {
|
|
315
|
+
try {
|
|
316
|
+
const parsed = JSON.parse(buf.toString("utf-8"));
|
|
317
|
+
if (parsed.status === "failed" || parsed.error) {
|
|
318
|
+
throw new Error(`Query failed: ${parsed.error || "unknown error"}`);
|
|
319
|
+
}
|
|
320
|
+
} catch (e) {
|
|
321
|
+
if (e.message.startsWith("Query failed:")) throw e;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
var EXT_TO_FORMAT = {
|
|
326
|
+
".csv": "csv",
|
|
327
|
+
".tsv": "tsv",
|
|
328
|
+
".json": "json",
|
|
329
|
+
".xlsx": "xlsx"
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// src/api/dataset.ts
|
|
333
|
+
var DatasetApi = class {
|
|
334
|
+
constructor(client) {
|
|
335
|
+
this.client = client;
|
|
336
|
+
}
|
|
337
|
+
async query(datasetQuery) {
|
|
338
|
+
return this.client.post("/api/dataset", datasetQuery);
|
|
339
|
+
}
|
|
340
|
+
async queryNative(database, sql, templateTags) {
|
|
341
|
+
return this.query({
|
|
342
|
+
type: "native",
|
|
343
|
+
database,
|
|
344
|
+
native: {
|
|
345
|
+
query: sql,
|
|
346
|
+
"template-tags": templateTags ?? {}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
async export(datasetQuery, format) {
|
|
351
|
+
const res = await this.client.requestRaw(
|
|
352
|
+
"POST",
|
|
353
|
+
`/api/dataset/${format}`,
|
|
354
|
+
datasetQuery
|
|
355
|
+
);
|
|
356
|
+
if (!res.ok) {
|
|
357
|
+
throw new Error(`Export failed: ${res.status} ${await res.text()}`);
|
|
358
|
+
}
|
|
359
|
+
return res.text();
|
|
360
|
+
}
|
|
361
|
+
async exportBinary(datasetQuery, format) {
|
|
362
|
+
const res = await this.client.requestFormExport(
|
|
363
|
+
`/api/dataset/${format}`,
|
|
364
|
+
{ query: JSON.stringify(datasetQuery) }
|
|
365
|
+
);
|
|
366
|
+
if (!res.ok) {
|
|
367
|
+
throw new Error(`Export failed: ${res.status} ${await res.text()}`);
|
|
368
|
+
}
|
|
369
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
370
|
+
checkExportError(buf, format);
|
|
371
|
+
return buf;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// src/client.ts
|
|
376
|
+
var MetabaseClient = class {
|
|
377
|
+
domain;
|
|
378
|
+
sessionToken;
|
|
379
|
+
apiKey;
|
|
380
|
+
profile;
|
|
381
|
+
constructor(profile) {
|
|
382
|
+
this.profile = profile;
|
|
383
|
+
this.domain = profile.domain.replace(/\/+$/, "");
|
|
384
|
+
if (profile.auth.method === "session") {
|
|
385
|
+
this.sessionToken = profile.auth.sessionToken;
|
|
386
|
+
} else {
|
|
387
|
+
this.apiKey = profile.auth.apiKey;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
getHeaders() {
|
|
391
|
+
const headers = {
|
|
392
|
+
"Content-Type": "application/json"
|
|
393
|
+
};
|
|
394
|
+
if (this.apiKey) {
|
|
395
|
+
headers["X-Api-Key"] = this.apiKey;
|
|
396
|
+
} else if (this.sessionToken) {
|
|
397
|
+
headers["X-Metabase-Session"] = this.sessionToken;
|
|
398
|
+
}
|
|
399
|
+
return headers;
|
|
400
|
+
}
|
|
401
|
+
async request(method, path2, body, params) {
|
|
402
|
+
let url = `${this.domain}${path2}`;
|
|
403
|
+
if (params) {
|
|
404
|
+
const qs = new URLSearchParams(params).toString();
|
|
405
|
+
url += `?${qs}`;
|
|
406
|
+
}
|
|
407
|
+
const res = await fetch(url, {
|
|
408
|
+
method,
|
|
409
|
+
headers: this.getHeaders(),
|
|
410
|
+
body: body ? JSON.stringify(body) : void 0
|
|
411
|
+
});
|
|
412
|
+
if (res.status === 401 && this.profile.auth.method === "session") {
|
|
413
|
+
await this.login();
|
|
414
|
+
const retryRes = await fetch(url, {
|
|
415
|
+
method,
|
|
416
|
+
headers: this.getHeaders(),
|
|
417
|
+
body: body ? JSON.stringify(body) : void 0
|
|
418
|
+
});
|
|
419
|
+
if (!retryRes.ok) {
|
|
420
|
+
const err = await retryRes.text();
|
|
421
|
+
throw new Error(`${retryRes.status} ${retryRes.statusText}: ${err}`);
|
|
422
|
+
}
|
|
423
|
+
const retryText = await retryRes.text();
|
|
424
|
+
if (!retryText) return void 0;
|
|
425
|
+
return JSON.parse(retryText);
|
|
426
|
+
}
|
|
427
|
+
if (!res.ok) {
|
|
428
|
+
const err = await res.text();
|
|
429
|
+
throw new Error(`${res.status} ${res.statusText}: ${err}`);
|
|
430
|
+
}
|
|
431
|
+
const text = await res.text();
|
|
432
|
+
if (!text) return void 0;
|
|
433
|
+
return JSON.parse(text);
|
|
434
|
+
}
|
|
435
|
+
async get(path2, params) {
|
|
436
|
+
return this.request("GET", path2, void 0, params);
|
|
437
|
+
}
|
|
438
|
+
async post(path2, body) {
|
|
439
|
+
return this.request("POST", path2, body);
|
|
440
|
+
}
|
|
441
|
+
async put(path2, body) {
|
|
442
|
+
return this.request("PUT", path2, body);
|
|
443
|
+
}
|
|
444
|
+
async delete(path2) {
|
|
445
|
+
return this.request("DELETE", path2);
|
|
446
|
+
}
|
|
447
|
+
async requestFormExport(path2, fields) {
|
|
448
|
+
const url = `${this.domain}${path2}`;
|
|
449
|
+
const headers = {};
|
|
450
|
+
if (this.apiKey) {
|
|
451
|
+
headers["X-Api-Key"] = this.apiKey;
|
|
452
|
+
} else if (this.sessionToken) {
|
|
453
|
+
headers["X-Metabase-Session"] = this.sessionToken;
|
|
454
|
+
}
|
|
455
|
+
const body = new URLSearchParams(fields);
|
|
456
|
+
const res = await fetch(url, { method: "POST", headers, body });
|
|
457
|
+
if (res.status === 401 && this.profile.auth.method === "session") {
|
|
458
|
+
await this.login();
|
|
459
|
+
if (this.sessionToken) {
|
|
460
|
+
headers["X-Metabase-Session"] = this.sessionToken;
|
|
461
|
+
}
|
|
462
|
+
return fetch(url, { method: "POST", headers, body: new URLSearchParams(fields) });
|
|
463
|
+
}
|
|
464
|
+
return res;
|
|
465
|
+
}
|
|
466
|
+
async requestRaw(method, path2, body) {
|
|
467
|
+
const url = `${this.domain}${path2}`;
|
|
468
|
+
const res = await fetch(url, {
|
|
469
|
+
method,
|
|
470
|
+
headers: this.getHeaders(),
|
|
471
|
+
body: body ? JSON.stringify(body) : void 0
|
|
472
|
+
});
|
|
473
|
+
if (res.status === 401 && this.profile.auth.method === "session") {
|
|
474
|
+
await this.login();
|
|
475
|
+
return fetch(url, {
|
|
476
|
+
method,
|
|
477
|
+
headers: this.getHeaders(),
|
|
478
|
+
body: body ? JSON.stringify(body) : void 0
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return res;
|
|
482
|
+
}
|
|
483
|
+
async login() {
|
|
484
|
+
const auth = this.profile.auth;
|
|
485
|
+
const res = await fetch(`${this.domain}/api/session`, {
|
|
486
|
+
method: "POST",
|
|
487
|
+
headers: { "Content-Type": "application/json" },
|
|
488
|
+
body: JSON.stringify({ username: auth.email, password: auth.password })
|
|
489
|
+
});
|
|
490
|
+
if (!res.ok) {
|
|
491
|
+
const err = await res.text();
|
|
492
|
+
throw new Error(`Login failed: ${res.status} ${err}`);
|
|
493
|
+
}
|
|
494
|
+
const session = await res.json();
|
|
495
|
+
this.sessionToken = session.id;
|
|
496
|
+
updateProfile(this.profile.name, {
|
|
497
|
+
auth: { ...auth, sessionToken: session.id }
|
|
498
|
+
});
|
|
499
|
+
const user = await this.get("/api/user/current");
|
|
500
|
+
const cachedUser = {
|
|
501
|
+
id: user.id,
|
|
502
|
+
email: user.email,
|
|
503
|
+
first_name: user.first_name,
|
|
504
|
+
last_name: user.last_name,
|
|
505
|
+
is_superuser: user.is_superuser
|
|
506
|
+
};
|
|
507
|
+
updateProfile(this.profile.name, { user: cachedUser });
|
|
508
|
+
this.profile.user = cachedUser;
|
|
509
|
+
return session;
|
|
510
|
+
}
|
|
511
|
+
async logout() {
|
|
512
|
+
await this.delete("/api/session");
|
|
513
|
+
this.sessionToken = void 0;
|
|
514
|
+
if (this.profile.auth.method === "session") {
|
|
515
|
+
updateProfile(this.profile.name, {
|
|
516
|
+
auth: { ...this.profile.auth, sessionToken: void 0 }
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async ensureAuthenticated() {
|
|
521
|
+
if (this.apiKey) return;
|
|
522
|
+
if (!this.sessionToken) {
|
|
523
|
+
await this.login();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
getProfile() {
|
|
527
|
+
return this.profile;
|
|
528
|
+
}
|
|
529
|
+
getUserId() {
|
|
530
|
+
return this.profile.user?.id ?? null;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// src/commands/helpers.ts
|
|
535
|
+
async function resolveClient() {
|
|
536
|
+
const profile = getActiveProfile();
|
|
537
|
+
if (!profile) {
|
|
538
|
+
console.error("No active profile. Run: metabase-cli profile add <name>");
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
const client = new MetabaseClient(profile);
|
|
542
|
+
await client.ensureAuthenticated();
|
|
543
|
+
return client;
|
|
544
|
+
}
|
|
545
|
+
function resolveDb(optDb) {
|
|
546
|
+
if (optDb !== void 0) return optDb;
|
|
547
|
+
const profile = getActiveProfile();
|
|
548
|
+
if (profile?.defaultDb) return profile.defaultDb;
|
|
549
|
+
console.error("Error: --db is required (or set a default with: metabase-cli profile set-default-db <id>)");
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
function isUnsafe(cmd, localFlag) {
|
|
553
|
+
if (localFlag) return true;
|
|
554
|
+
if (process.env.METABASE_UNSAFE === "1") return true;
|
|
555
|
+
let current = cmd;
|
|
556
|
+
while (current) {
|
|
557
|
+
if (current.opts().unsafe) return true;
|
|
558
|
+
current = current.parent;
|
|
559
|
+
}
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/commands/query.ts
|
|
564
|
+
function queryCommand() {
|
|
565
|
+
const cmd = new import_commander2.Command("query").description("Run queries against a database").addHelpText("after", `
|
|
566
|
+
Examples:
|
|
567
|
+
$ metabase-cli query run --sql "SELECT * FROM users LIMIT 10" --db 1
|
|
568
|
+
$ metabase-cli query run --sql "SELECT count(*) FROM orders" --db 1 --format json
|
|
569
|
+
$ metabase-cli query run --sql "SELECT * FROM products" --db 1 --format csv
|
|
570
|
+
$ metabase-cli query run --sql "SELECT * FROM users" --db 1 --columns "id,email" --limit 5`);
|
|
571
|
+
cmd.command("run").description("Execute a SQL query").requiredOption("--sql <sql>", "SQL query to execute").option("--db <id>", "Database ID (uses profile default if not set)", parseInt).option("--format <format>", "Output format: table, json, csv, tsv, xlsx", "table").option("--output <file>", "Write output to a file (format auto-detected from extension)").option("--columns <cols>", "Comma-separated column names to display").option("--limit <n>", "Limit number of rows", parseInt).addHelpText("after", `
|
|
572
|
+
Examples:
|
|
573
|
+
$ metabase-cli query run --sql "SELECT * FROM users LIMIT 10" --db 1
|
|
574
|
+
$ metabase-cli query run --sql "SELECT count(*) FROM orders" --db 1 --format json
|
|
575
|
+
$ metabase-cli query run --sql "SELECT * FROM products" --db 2 --format csv > products.csv
|
|
576
|
+
$ metabase-cli query run --sql "SELECT * FROM products" --db 2 --output products.xlsx
|
|
577
|
+
$ metabase-cli query run --sql "SELECT * FROM products" --db 2 --output results.csv`).action(async (opts) => {
|
|
578
|
+
const client = await resolveClient();
|
|
579
|
+
const api = new DatasetApi(client);
|
|
580
|
+
let sql = opts.sql;
|
|
581
|
+
if (opts.limit) {
|
|
582
|
+
sql = `SELECT * FROM (${sql}) _q LIMIT ${opts.limit}`;
|
|
583
|
+
}
|
|
584
|
+
const db = resolveDb(opts.db);
|
|
585
|
+
let format = opts.format;
|
|
586
|
+
const outputPath = opts.output ? (0, import_node_path.resolve)(opts.output) : null;
|
|
587
|
+
if (outputPath) {
|
|
588
|
+
const ext = (0, import_node_path.extname)(outputPath).toLowerCase();
|
|
589
|
+
const inferredFormat = EXT_TO_FORMAT[ext];
|
|
590
|
+
if (inferredFormat && format === "table") {
|
|
591
|
+
format = inferredFormat;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (outputPath) {
|
|
595
|
+
if (format === "xlsx" || format === "csv" || format === "json") {
|
|
596
|
+
if (opts.columns) {
|
|
597
|
+
console.warn("Warning: --columns is not supported with native export (csv/json/xlsx). All columns will be exported.");
|
|
598
|
+
}
|
|
599
|
+
const datasetQuery = {
|
|
600
|
+
type: "native",
|
|
601
|
+
database: db,
|
|
602
|
+
native: { query: sql, "template-tags": {} }
|
|
603
|
+
};
|
|
604
|
+
const data = await api.exportBinary(datasetQuery, format);
|
|
605
|
+
(0, import_node_fs.writeFileSync)(outputPath, data);
|
|
606
|
+
console.log(`Exported to ${outputPath}`);
|
|
607
|
+
} else {
|
|
608
|
+
const result2 = await api.queryNative(db, sql);
|
|
609
|
+
if (result2.status === "failed") {
|
|
610
|
+
console.error("Query failed:", result2.error);
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
const columns2 = opts.columns?.split(",");
|
|
614
|
+
const output = formatDatasetResponse(result2, format, columns2);
|
|
615
|
+
(0, import_node_fs.writeFileSync)(outputPath, output, "utf-8");
|
|
616
|
+
console.log(`Exported ${result2.row_count} row(s) to ${outputPath}`);
|
|
617
|
+
}
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (format === "xlsx") {
|
|
621
|
+
console.error("Error: xlsx format requires --output <file>");
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
const result = await api.queryNative(db, sql);
|
|
625
|
+
if (result.status === "failed") {
|
|
626
|
+
console.error("Query failed:", result.error);
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
const columns = opts.columns?.split(",");
|
|
630
|
+
console.log(
|
|
631
|
+
formatDatasetResponse(result, format, columns)
|
|
632
|
+
);
|
|
633
|
+
console.log(`
|
|
634
|
+
${result.row_count} row(s) returned.`);
|
|
635
|
+
});
|
|
636
|
+
return cmd;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/commands/question.ts
|
|
640
|
+
var import_commander3 = require("commander");
|
|
641
|
+
var import_node_fs2 = require("fs");
|
|
642
|
+
var import_node_path2 = require("path");
|
|
643
|
+
|
|
644
|
+
// src/api/card.ts
|
|
645
|
+
var CardApi = class {
|
|
646
|
+
constructor(client) {
|
|
647
|
+
this.client = client;
|
|
648
|
+
}
|
|
649
|
+
async list(params) {
|
|
650
|
+
return this.client.get("/api/card", params);
|
|
651
|
+
}
|
|
652
|
+
async get(id) {
|
|
653
|
+
return this.client.get(`/api/card/${id}`);
|
|
654
|
+
}
|
|
655
|
+
async create(params) {
|
|
656
|
+
return this.client.post("/api/card", params);
|
|
657
|
+
}
|
|
658
|
+
async update(id, params) {
|
|
659
|
+
return this.client.put(`/api/card/${id}`, params);
|
|
660
|
+
}
|
|
661
|
+
async delete(id) {
|
|
662
|
+
await this.client.delete(`/api/card/${id}`);
|
|
663
|
+
}
|
|
664
|
+
async copy(id, overrides) {
|
|
665
|
+
return this.client.post(`/api/card/${id}/copy`, overrides);
|
|
666
|
+
}
|
|
667
|
+
async query(id, parameters) {
|
|
668
|
+
return this.client.post(`/api/card/${id}/query`, {
|
|
669
|
+
parameters: parameters ?? []
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
async queryExport(id, format, parameters) {
|
|
673
|
+
const res = await this.client.requestRaw(
|
|
674
|
+
"POST",
|
|
675
|
+
`/api/card/${id}/query/${format}`,
|
|
676
|
+
{ parameters: parameters ?? [] }
|
|
677
|
+
);
|
|
678
|
+
if (!res.ok) {
|
|
679
|
+
throw new Error(`Export failed: ${res.status} ${await res.text()}`);
|
|
680
|
+
}
|
|
681
|
+
return res.text();
|
|
682
|
+
}
|
|
683
|
+
async queryExportBinary(id, format, parameters) {
|
|
684
|
+
const fields = {};
|
|
685
|
+
if (parameters && parameters.length > 0) {
|
|
686
|
+
fields.parameters = JSON.stringify(parameters);
|
|
687
|
+
}
|
|
688
|
+
const res = await this.client.requestFormExport(
|
|
689
|
+
`/api/card/${id}/query/${format}`,
|
|
690
|
+
fields
|
|
691
|
+
);
|
|
692
|
+
if (!res.ok) {
|
|
693
|
+
throw new Error(`Export failed: ${res.status} ${await res.text()}`);
|
|
694
|
+
}
|
|
695
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
696
|
+
checkExportError(buf, format);
|
|
697
|
+
return buf;
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// src/safety/guard.ts
|
|
702
|
+
var SafetyGuard = class {
|
|
703
|
+
client;
|
|
704
|
+
unsafe;
|
|
705
|
+
constructor(client, unsafe = false) {
|
|
706
|
+
this.client = client;
|
|
707
|
+
this.unsafe = unsafe;
|
|
708
|
+
}
|
|
709
|
+
async checkOwnership(entityType, entityId) {
|
|
710
|
+
const pathMap = {
|
|
711
|
+
card: `/api/card/${entityId}`,
|
|
712
|
+
dashboard: `/api/dashboard/${entityId}`,
|
|
713
|
+
snippet: `/api/native-query-snippet/${entityId}`,
|
|
714
|
+
collection: `/api/collection/${entityId}`
|
|
715
|
+
};
|
|
716
|
+
const path2 = pathMap[entityType];
|
|
717
|
+
if (!path2) {
|
|
718
|
+
throw new Error(`Unknown entity type: ${entityType}`);
|
|
719
|
+
}
|
|
720
|
+
const entity = await this.client.get(path2);
|
|
721
|
+
const userId = this.client.getUserId();
|
|
722
|
+
if (userId === null) {
|
|
723
|
+
throw new Error("No cached user ID. Run 'metabase-cli login' or 'metabase-cli whoami --refresh' first.");
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
owned: entity.creator_id === userId,
|
|
727
|
+
creatorId: entity.creator_id
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
async guard(entityType, entityId, action, fn) {
|
|
731
|
+
if (this.unsafe) {
|
|
732
|
+
return fn();
|
|
733
|
+
}
|
|
734
|
+
const { owned, creatorId } = await this.checkOwnership(entityType, entityId);
|
|
735
|
+
if (!owned) {
|
|
736
|
+
const userId = this.client.getUserId();
|
|
737
|
+
throw new Error(
|
|
738
|
+
`Safe mode: Cannot ${action} ${entityType} #${entityId} \u2014 owned by user #${creatorId}, you are user #${userId}. Use --unsafe to bypass.`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
return fn();
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// src/commands/question.ts
|
|
746
|
+
var TAG_TYPE_TO_PARAM_TYPE = {
|
|
747
|
+
date: "date/single",
|
|
748
|
+
text: "string/=",
|
|
749
|
+
number: "number/="
|
|
750
|
+
};
|
|
751
|
+
function buildParametersFromTags(tags) {
|
|
752
|
+
if (Object.keys(tags).length === 0) return [];
|
|
753
|
+
return Object.entries(tags).filter(([, tag]) => {
|
|
754
|
+
return tag.type !== "snippet" && tag.type !== "card";
|
|
755
|
+
}).map(([name, tag]) => {
|
|
756
|
+
const isDimension = tag.type === "dimension";
|
|
757
|
+
const paramType = isDimension ? tag["widget-type"] || "string/=" : TAG_TYPE_TO_PARAM_TYPE[tag.type] || "string/=";
|
|
758
|
+
const param = {
|
|
759
|
+
id: tag.id || crypto.randomUUID(),
|
|
760
|
+
type: paramType,
|
|
761
|
+
target: isDimension ? ["dimension", ["template-tag", name]] : ["variable", ["template-tag", name]],
|
|
762
|
+
name: tag["display-name"] || name,
|
|
763
|
+
slug: name
|
|
764
|
+
};
|
|
765
|
+
if (tag.default !== void 0) {
|
|
766
|
+
param.default = tag.default;
|
|
767
|
+
}
|
|
768
|
+
return param;
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
function questionCommand() {
|
|
772
|
+
const cmd = new import_commander3.Command("question").description("Manage questions (saved cards)").addHelpText("after", `
|
|
773
|
+
Examples:
|
|
774
|
+
$ metabase-cli question list --filter mine
|
|
775
|
+
$ metabase-cli question show 42
|
|
776
|
+
$ metabase-cli question run 42 --format csv
|
|
777
|
+
$ metabase-cli question create --name "Active Users" --sql "SELECT * FROM users WHERE active" --db 1
|
|
778
|
+
$ metabase-cli question update 42 --name "New Name" --unsafe
|
|
779
|
+
$ metabase-cli question delete 42
|
|
780
|
+
$ metabase-cli question copy 42 --name "Copy" --collection 5`);
|
|
781
|
+
cmd.command("list").description("List questions").option("--filter <f>", "Filter: all, mine, bookmarked, archived").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
|
|
782
|
+
Examples:
|
|
783
|
+
$ metabase-cli question list
|
|
784
|
+
$ metabase-cli question list --filter mine
|
|
785
|
+
$ metabase-cli question list --format json`).action(async (opts) => {
|
|
786
|
+
const client = await resolveClient();
|
|
787
|
+
const api = new CardApi(client);
|
|
788
|
+
const params = {};
|
|
789
|
+
if (opts.filter) params.f = opts.filter;
|
|
790
|
+
const cards = await api.list(params);
|
|
791
|
+
if (opts.format === "json") {
|
|
792
|
+
console.log(formatJson(cards));
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
console.log(
|
|
796
|
+
formatEntityTable(cards, [
|
|
797
|
+
{ key: "id", header: "ID" },
|
|
798
|
+
{ key: "name", header: "Name" },
|
|
799
|
+
{ key: "display", header: "Display" },
|
|
800
|
+
{ key: "collection_id", header: "Collection" },
|
|
801
|
+
{ key: "creator_id", header: "Creator" }
|
|
802
|
+
])
|
|
803
|
+
);
|
|
804
|
+
});
|
|
805
|
+
cmd.command("show <id>").description("Show question details").addHelpText("after", `
|
|
806
|
+
Examples:
|
|
807
|
+
$ metabase-cli question show 42`).action(async (id) => {
|
|
808
|
+
const client = await resolveClient();
|
|
809
|
+
const api = new CardApi(client);
|
|
810
|
+
const card = await api.get(parseInt(id));
|
|
811
|
+
console.log(formatJson(card));
|
|
812
|
+
});
|
|
813
|
+
cmd.command("run <id>").description("Execute a saved question").option("--format <format>", "Output format: table, json, csv, tsv, xlsx", "table").option("--output <file>", "Write output to a file (format auto-detected from extension)").option("--columns <cols>", "Comma-separated column names").option("--params <json>", `Parameter values as JSON, e.g. '{"start_date":"2025-01-01"}'`).addHelpText("after", `
|
|
814
|
+
Examples:
|
|
815
|
+
$ metabase-cli question run 42
|
|
816
|
+
$ metabase-cli question run 42 --format csv
|
|
817
|
+
$ metabase-cli question run 42 --columns "id,name,email"
|
|
818
|
+
$ metabase-cli question run 42 --params '{"start_date":"2025-01-01"}'
|
|
819
|
+
$ metabase-cli question run 42 --output results.xlsx
|
|
820
|
+
$ metabase-cli question run 42 --output results.csv`).action(async (id, opts) => {
|
|
821
|
+
const client = await resolveClient();
|
|
822
|
+
const api = new CardApi(client);
|
|
823
|
+
const cardId = parseInt(id);
|
|
824
|
+
let format = opts.format;
|
|
825
|
+
const outputPath = opts.output ? (0, import_node_path2.resolve)(opts.output) : null;
|
|
826
|
+
if (outputPath) {
|
|
827
|
+
const ext = (0, import_node_path2.extname)(outputPath).toLowerCase();
|
|
828
|
+
const inferredFormat = EXT_TO_FORMAT[ext];
|
|
829
|
+
if (inferredFormat && format === "table") {
|
|
830
|
+
format = inferredFormat;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
const card = await api.get(cardId);
|
|
834
|
+
const cardParams = card.parameters || [];
|
|
835
|
+
let paramsInput = {};
|
|
836
|
+
if (opts.params) {
|
|
837
|
+
try {
|
|
838
|
+
paramsInput = JSON.parse(opts.params);
|
|
839
|
+
} catch {
|
|
840
|
+
console.error("Error: --params must be valid JSON");
|
|
841
|
+
process.exit(1);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const parameterValues = cardParams.map((p) => {
|
|
845
|
+
const value = paramsInput[p.slug] ?? p.default;
|
|
846
|
+
if (value === void 0) return null;
|
|
847
|
+
return {
|
|
848
|
+
id: p.id,
|
|
849
|
+
type: p.type,
|
|
850
|
+
target: p.target,
|
|
851
|
+
value
|
|
852
|
+
};
|
|
853
|
+
}).filter(Boolean);
|
|
854
|
+
if (outputPath) {
|
|
855
|
+
if (format === "xlsx" || format === "csv" || format === "json") {
|
|
856
|
+
if (opts.columns) {
|
|
857
|
+
console.warn("Warning: --columns is not supported with native export (csv/json/xlsx). All columns will be exported.");
|
|
858
|
+
}
|
|
859
|
+
const data = await api.queryExportBinary(cardId, format, parameterValues);
|
|
860
|
+
(0, import_node_fs2.writeFileSync)(outputPath, data);
|
|
861
|
+
console.log(`Exported to ${outputPath}`);
|
|
862
|
+
} else {
|
|
863
|
+
const result2 = await api.query(cardId, parameterValues);
|
|
864
|
+
if (result2.status === "failed") {
|
|
865
|
+
console.error("Query failed:", result2.error);
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
const columns2 = opts.columns?.split(",");
|
|
869
|
+
const output = formatDatasetResponse(result2, format, columns2);
|
|
870
|
+
(0, import_node_fs2.writeFileSync)(outputPath, output, "utf-8");
|
|
871
|
+
console.log(`Exported ${result2.row_count} row(s) to ${outputPath}`);
|
|
872
|
+
}
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (format === "xlsx") {
|
|
876
|
+
console.error("Error: xlsx format requires --output <file>");
|
|
877
|
+
process.exit(1);
|
|
878
|
+
}
|
|
879
|
+
const result = await api.query(cardId, parameterValues);
|
|
880
|
+
if (result.status === "failed") {
|
|
881
|
+
console.error("Query failed:", result.error);
|
|
882
|
+
process.exit(1);
|
|
883
|
+
}
|
|
884
|
+
const columns = opts.columns?.split(",");
|
|
885
|
+
console.log(
|
|
886
|
+
formatDatasetResponse(result, format, columns)
|
|
887
|
+
);
|
|
888
|
+
console.log(`
|
|
889
|
+
${result.row_count} row(s) returned.`);
|
|
890
|
+
});
|
|
891
|
+
cmd.command("create").description("Create a new question").requiredOption("--name <name>", "Question name").requiredOption("--sql <sql>", "SQL query").option("--db <id>", "Database ID (uses profile default if not set)", parseInt).option("--description <desc>", "Description").option("--collection <id>", "Collection ID", parseInt).option("--display <type>", "Display type (table, line, bar, pie, scalar, etc.)", "table").option("--viz <json>", "Visualization settings as JSON string").option("--template-tags <json>", "Template tags as JSON string for parameterized queries").addHelpText("after", `
|
|
892
|
+
Display types: table, line, bar, area, pie, scalar, row, scatter, funnel, map, pivot, progress, gauge, waterfall
|
|
893
|
+
|
|
894
|
+
Examples:
|
|
895
|
+
$ metabase-cli question create --name "Active Users" --sql "SELECT * FROM users WHERE active = true" --db 1
|
|
896
|
+
$ metabase-cli question create --name "Revenue" --sql "SELECT sum(amount) FROM orders" --db 1 --collection 5
|
|
897
|
+
$ metabase-cli question create --name "Revenue Trend" --sql "SELECT date, sum(amount) FROM orders GROUP BY date" --display line
|
|
898
|
+
$ metabase-cli question create --name "Revenue Trend" --sql "..." --display line --viz '{"graph.show_values":true}'
|
|
899
|
+
$ metabase-cli question create --name "Users Since" --sql "SELECT * FROM users WHERE created_at >= {{start_date}}" --template-tags '{"start_date":{"type":"date","name":"start_date","display-name":"Start Date","default":"2024-01-01"}}'`).action(async (opts) => {
|
|
900
|
+
const client = await resolveClient();
|
|
901
|
+
const api = new CardApi(client);
|
|
902
|
+
const db = resolveDb(opts.db);
|
|
903
|
+
let vizSettings = {};
|
|
904
|
+
if (opts.viz) {
|
|
905
|
+
try {
|
|
906
|
+
vizSettings = JSON.parse(opts.viz);
|
|
907
|
+
} catch {
|
|
908
|
+
console.error("Error: --viz must be valid JSON");
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
let templateTags = {};
|
|
913
|
+
if (opts.templateTags) {
|
|
914
|
+
try {
|
|
915
|
+
templateTags = JSON.parse(opts.templateTags);
|
|
916
|
+
} catch {
|
|
917
|
+
console.error("Error: --template-tags must be valid JSON");
|
|
918
|
+
process.exit(1);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
for (const tag of Object.values(templateTags)) {
|
|
922
|
+
if (!tag.id) {
|
|
923
|
+
tag.id = crypto.randomUUID();
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const parameters = buildParametersFromTags(templateTags);
|
|
927
|
+
const card = await api.create({
|
|
928
|
+
name: opts.name,
|
|
929
|
+
display: opts.display,
|
|
930
|
+
description: opts.description,
|
|
931
|
+
collection_id: opts.collection,
|
|
932
|
+
visualization_settings: vizSettings,
|
|
933
|
+
parameters,
|
|
934
|
+
dataset_query: {
|
|
935
|
+
type: "native",
|
|
936
|
+
database: db,
|
|
937
|
+
native: {
|
|
938
|
+
query: opts.sql,
|
|
939
|
+
"template-tags": Object.keys(templateTags).length > 0 ? templateTags : void 0
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
console.log(`Question #${card.id} "${card.name}" created.`);
|
|
944
|
+
});
|
|
945
|
+
cmd.command("update <id>").description("Update a question (safe mode by default)").option("--name <name>", "New name").option("--description <desc>", "New description").option("--collection <id>", "Move to collection", parseInt).option("--sql <sql>", "New SQL query").option("--display <type>", "Change display type (table, line, bar, pie, scalar, etc.)").option("--viz <json>", "Visualization settings as JSON (merged with existing)").option("--unsafe", "Bypass safe mode", false).addHelpText("after", `
|
|
946
|
+
Safe mode blocks updates to questions you didn't create. Use --unsafe to bypass.
|
|
947
|
+
|
|
948
|
+
Examples:
|
|
949
|
+
$ metabase-cli question update 42 --name "New Name"
|
|
950
|
+
$ metabase-cli question update 42 --sql "SELECT * FROM users WHERE active" --unsafe
|
|
951
|
+
$ metabase-cli question update 42 --display line
|
|
952
|
+
$ metabase-cli question update 42 --viz '{"graph.show_values":true,"graph.dimensions":["date"]}'`).action(async function(id, opts) {
|
|
953
|
+
const client = await resolveClient();
|
|
954
|
+
const api = new CardApi(client);
|
|
955
|
+
const guard = new SafetyGuard(client, isUnsafe(this, opts.unsafe));
|
|
956
|
+
const cardId = parseInt(id);
|
|
957
|
+
await guard.guard("card", cardId, "update", async () => {
|
|
958
|
+
const updates = {};
|
|
959
|
+
if (opts.name) updates.name = opts.name;
|
|
960
|
+
if (opts.description) updates.description = opts.description;
|
|
961
|
+
if (opts.collection !== void 0) updates.collection_id = opts.collection;
|
|
962
|
+
if (opts.display) updates.display = opts.display;
|
|
963
|
+
if (opts.viz) {
|
|
964
|
+
let vizSettings;
|
|
965
|
+
try {
|
|
966
|
+
vizSettings = JSON.parse(opts.viz);
|
|
967
|
+
} catch {
|
|
968
|
+
console.error("Error: --viz must be valid JSON");
|
|
969
|
+
process.exit(1);
|
|
970
|
+
}
|
|
971
|
+
const existing = await api.get(cardId);
|
|
972
|
+
updates.visualization_settings = {
|
|
973
|
+
...existing.visualization_settings,
|
|
974
|
+
...vizSettings
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
if (opts.sql) {
|
|
978
|
+
const existing = await api.get(cardId);
|
|
979
|
+
updates.dataset_query = {
|
|
980
|
+
...existing.dataset_query,
|
|
981
|
+
native: {
|
|
982
|
+
...existing.dataset_query.native,
|
|
983
|
+
query: opts.sql
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
const card = await api.update(cardId, updates);
|
|
988
|
+
console.log(`Question #${card.id} "${card.name}" updated.`);
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
cmd.command("delete <id>").description("Delete a question (safe mode by default)").option("--unsafe", "Bypass safe mode", false).addHelpText("after", `
|
|
992
|
+
Examples:
|
|
993
|
+
$ metabase-cli question delete 42
|
|
994
|
+
$ metabase-cli question delete 42 --unsafe`).action(async function(id, opts) {
|
|
995
|
+
const client = await resolveClient();
|
|
996
|
+
const api = new CardApi(client);
|
|
997
|
+
const guard = new SafetyGuard(client, isUnsafe(this, opts.unsafe));
|
|
998
|
+
const cardId = parseInt(id);
|
|
999
|
+
await guard.guard("card", cardId, "delete", async () => {
|
|
1000
|
+
await api.delete(cardId);
|
|
1001
|
+
console.log(`Question #${cardId} deleted.`);
|
|
1002
|
+
});
|
|
1003
|
+
});
|
|
1004
|
+
cmd.command("copy <id>").description("Copy a question").option("--name <name>", "Name for the copy").option("--collection <id>", "Target collection", parseInt).addHelpText("after", `
|
|
1005
|
+
Examples:
|
|
1006
|
+
$ metabase-cli question copy 42
|
|
1007
|
+
$ metabase-cli question copy 42 --name "Copy of Revenue" --collection 10`).action(async (id, opts) => {
|
|
1008
|
+
const client = await resolveClient();
|
|
1009
|
+
const api = new CardApi(client);
|
|
1010
|
+
const overrides = {};
|
|
1011
|
+
if (opts.name) overrides.name = opts.name;
|
|
1012
|
+
if (opts.collection !== void 0) overrides.collection_id = opts.collection;
|
|
1013
|
+
const card = await api.copy(parseInt(id), overrides);
|
|
1014
|
+
console.log(`Question #${card.id} "${card.name}" created (copy).`);
|
|
1015
|
+
});
|
|
1016
|
+
return cmd;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/commands/dashboard.ts
|
|
1020
|
+
var import_commander4 = require("commander");
|
|
1021
|
+
|
|
1022
|
+
// src/api/dashboard.ts
|
|
1023
|
+
var DashboardApi = class {
|
|
1024
|
+
constructor(client) {
|
|
1025
|
+
this.client = client;
|
|
1026
|
+
}
|
|
1027
|
+
async list(params) {
|
|
1028
|
+
return this.client.get("/api/dashboard", params);
|
|
1029
|
+
}
|
|
1030
|
+
async get(id) {
|
|
1031
|
+
return this.client.get(`/api/dashboard/${id}`);
|
|
1032
|
+
}
|
|
1033
|
+
async create(params) {
|
|
1034
|
+
return this.client.post("/api/dashboard", params);
|
|
1035
|
+
}
|
|
1036
|
+
async update(id, params) {
|
|
1037
|
+
return this.client.put(`/api/dashboard/${id}`, params);
|
|
1038
|
+
}
|
|
1039
|
+
async delete(id) {
|
|
1040
|
+
await this.client.delete(`/api/dashboard/${id}`);
|
|
1041
|
+
}
|
|
1042
|
+
async copy(id, overrides) {
|
|
1043
|
+
return this.client.post(`/api/dashboard/${id}/copy`, overrides);
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
// src/commands/dashboard.ts
|
|
1048
|
+
function dashboardCommand() {
|
|
1049
|
+
const cmd = new import_commander4.Command("dashboard").description("Manage dashboards").addHelpText("after", `
|
|
1050
|
+
Examples:
|
|
1051
|
+
$ metabase-cli dashboard list
|
|
1052
|
+
$ metabase-cli dashboard show 7
|
|
1053
|
+
$ metabase-cli dashboard create --name "Sales Overview" --collection 5
|
|
1054
|
+
$ metabase-cli dashboard update 7 --name "Updated" --unsafe
|
|
1055
|
+
$ metabase-cli dashboard delete 7
|
|
1056
|
+
$ metabase-cli dashboard copy 7 --name "Sales Overview (copy)"`);
|
|
1057
|
+
cmd.command("list").description("List dashboards").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
|
|
1058
|
+
Examples:
|
|
1059
|
+
$ metabase-cli dashboard list
|
|
1060
|
+
$ metabase-cli dashboard list --format json`).action(async (opts) => {
|
|
1061
|
+
const client = await resolveClient();
|
|
1062
|
+
const api = new DashboardApi(client);
|
|
1063
|
+
const dashboards = await api.list();
|
|
1064
|
+
if (opts.format === "json") {
|
|
1065
|
+
console.log(formatJson(dashboards));
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
console.log(
|
|
1069
|
+
formatEntityTable(dashboards, [
|
|
1070
|
+
{ key: "id", header: "ID" },
|
|
1071
|
+
{ key: "name", header: "Name" },
|
|
1072
|
+
{ key: "collection_id", header: "Collection" },
|
|
1073
|
+
{ key: "creator_id", header: "Creator" }
|
|
1074
|
+
])
|
|
1075
|
+
);
|
|
1076
|
+
});
|
|
1077
|
+
cmd.command("show <id>").description("Show dashboard details").addHelpText("after", `
|
|
1078
|
+
Examples:
|
|
1079
|
+
$ metabase-cli dashboard show 7`).action(async (id) => {
|
|
1080
|
+
const client = await resolveClient();
|
|
1081
|
+
const api = new DashboardApi(client);
|
|
1082
|
+
const dashboard = await api.get(parseInt(id));
|
|
1083
|
+
console.log(formatJson(dashboard));
|
|
1084
|
+
});
|
|
1085
|
+
cmd.command("create").description("Create a new dashboard").requiredOption("--name <name>", "Dashboard name").option("--description <desc>", "Description").option("--collection <id>", "Collection ID", parseInt).addHelpText("after", `
|
|
1086
|
+
Examples:
|
|
1087
|
+
$ metabase-cli dashboard create --name "Sales Overview"
|
|
1088
|
+
$ metabase-cli dashboard create --name "Q1 Report" --description "Quarterly report" --collection 5`).action(async (opts) => {
|
|
1089
|
+
const client = await resolveClient();
|
|
1090
|
+
const api = new DashboardApi(client);
|
|
1091
|
+
const dashboard = await api.create({
|
|
1092
|
+
name: opts.name,
|
|
1093
|
+
description: opts.description,
|
|
1094
|
+
collection_id: opts.collection
|
|
1095
|
+
});
|
|
1096
|
+
console.log(`Dashboard #${dashboard.id} "${dashboard.name}" created.`);
|
|
1097
|
+
});
|
|
1098
|
+
cmd.command("update <id>").description("Update a dashboard (safe mode by default)").option("--name <name>", "New name").option("--description <desc>", "New description").option("--collection <id>", "Move to collection", parseInt).option("--unsafe", "Bypass safe mode", false).addHelpText("after", `
|
|
1099
|
+
Safe mode blocks updates to dashboards you didn't create. Use --unsafe to bypass.
|
|
1100
|
+
|
|
1101
|
+
Examples:
|
|
1102
|
+
$ metabase-cli dashboard update 7 --name "Updated Name"
|
|
1103
|
+
$ metabase-cli dashboard update 7 --collection 10 --unsafe`).action(async function(id, opts) {
|
|
1104
|
+
const client = await resolveClient();
|
|
1105
|
+
const api = new DashboardApi(client);
|
|
1106
|
+
const guard = new SafetyGuard(client, isUnsafe(this, opts.unsafe));
|
|
1107
|
+
const dashId = parseInt(id);
|
|
1108
|
+
await guard.guard("dashboard", dashId, "update", async () => {
|
|
1109
|
+
const updates = {};
|
|
1110
|
+
if (opts.name) updates.name = opts.name;
|
|
1111
|
+
if (opts.description) updates.description = opts.description;
|
|
1112
|
+
if (opts.collection !== void 0) updates.collection_id = opts.collection;
|
|
1113
|
+
const dashboard = await api.update(dashId, updates);
|
|
1114
|
+
console.log(`Dashboard #${dashboard.id} "${dashboard.name}" updated.`);
|
|
1115
|
+
});
|
|
1116
|
+
});
|
|
1117
|
+
cmd.command("delete <id>").description("Delete a dashboard (safe mode by default)").option("--unsafe", "Bypass safe mode", false).addHelpText("after", `
|
|
1118
|
+
Examples:
|
|
1119
|
+
$ metabase-cli dashboard delete 7
|
|
1120
|
+
$ metabase-cli dashboard delete 7 --unsafe`).action(async function(id, opts) {
|
|
1121
|
+
const client = await resolveClient();
|
|
1122
|
+
const api = new DashboardApi(client);
|
|
1123
|
+
const guard = new SafetyGuard(client, isUnsafe(this, opts.unsafe));
|
|
1124
|
+
const dashId = parseInt(id);
|
|
1125
|
+
await guard.guard("dashboard", dashId, "delete", async () => {
|
|
1126
|
+
await api.delete(dashId);
|
|
1127
|
+
console.log(`Dashboard #${dashId} deleted.`);
|
|
1128
|
+
});
|
|
1129
|
+
});
|
|
1130
|
+
cmd.command("copy <id>").description("Copy a dashboard").option("--name <name>", "Name for the copy").option("--collection <id>", "Target collection", parseInt).addHelpText("after", `
|
|
1131
|
+
Examples:
|
|
1132
|
+
$ metabase-cli dashboard copy 7
|
|
1133
|
+
$ metabase-cli dashboard copy 7 --name "Sales Overview (copy)" --collection 10`).action(async (id, opts) => {
|
|
1134
|
+
const client = await resolveClient();
|
|
1135
|
+
const api = new DashboardApi(client);
|
|
1136
|
+
const overrides = {};
|
|
1137
|
+
if (opts.name) overrides.name = opts.name;
|
|
1138
|
+
if (opts.collection !== void 0) overrides.collection_id = opts.collection;
|
|
1139
|
+
const dashboard = await api.copy(parseInt(id), overrides);
|
|
1140
|
+
console.log(`Dashboard #${dashboard.id} "${dashboard.name}" created (copy).`);
|
|
1141
|
+
});
|
|
1142
|
+
return cmd;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/commands/collection.ts
|
|
1146
|
+
var import_commander5 = require("commander");
|
|
1147
|
+
|
|
1148
|
+
// src/api/collection.ts
|
|
1149
|
+
var CollectionApi = class {
|
|
1150
|
+
constructor(client) {
|
|
1151
|
+
this.client = client;
|
|
1152
|
+
}
|
|
1153
|
+
async list() {
|
|
1154
|
+
return this.client.get("/api/collection");
|
|
1155
|
+
}
|
|
1156
|
+
async tree() {
|
|
1157
|
+
return this.client.get("/api/collection/tree");
|
|
1158
|
+
}
|
|
1159
|
+
async get(id) {
|
|
1160
|
+
return this.client.get(`/api/collection/${id}`);
|
|
1161
|
+
}
|
|
1162
|
+
async items(id, params) {
|
|
1163
|
+
return this.client.get(`/api/collection/${id}/items`, params);
|
|
1164
|
+
}
|
|
1165
|
+
async create(params) {
|
|
1166
|
+
return this.client.post("/api/collection", params);
|
|
1167
|
+
}
|
|
1168
|
+
async update(id, params) {
|
|
1169
|
+
return this.client.put(`/api/collection/${id}`, params);
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
// src/commands/collection.ts
|
|
1174
|
+
function collectionCommand() {
|
|
1175
|
+
const cmd = new import_commander5.Command("collection").description("Manage collections").addHelpText("after", `
|
|
1176
|
+
Examples:
|
|
1177
|
+
$ metabase-cli collection list
|
|
1178
|
+
$ metabase-cli collection tree
|
|
1179
|
+
$ metabase-cli collection items 5 --models card
|
|
1180
|
+
$ metabase-cli collection create --name "Analytics" --parent 3`);
|
|
1181
|
+
cmd.command("list").description("List all collections").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
|
|
1182
|
+
Examples:
|
|
1183
|
+
$ metabase-cli collection list
|
|
1184
|
+
$ metabase-cli collection list --format json`).action(async (opts) => {
|
|
1185
|
+
const client = await resolveClient();
|
|
1186
|
+
const api = new CollectionApi(client);
|
|
1187
|
+
const collections = await api.list();
|
|
1188
|
+
if (opts.format === "json") {
|
|
1189
|
+
console.log(formatJson(collections));
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
console.log(
|
|
1193
|
+
formatEntityTable(collections, [
|
|
1194
|
+
{ key: "id", header: "ID" },
|
|
1195
|
+
{ key: "name", header: "Name" },
|
|
1196
|
+
{ key: "parent_id", header: "Parent" }
|
|
1197
|
+
])
|
|
1198
|
+
);
|
|
1199
|
+
});
|
|
1200
|
+
cmd.command("tree").description("Show collection hierarchy").addHelpText("after", `
|
|
1201
|
+
Examples:
|
|
1202
|
+
$ metabase-cli collection tree`).action(async () => {
|
|
1203
|
+
const client = await resolveClient();
|
|
1204
|
+
const api = new CollectionApi(client);
|
|
1205
|
+
const tree = await api.tree();
|
|
1206
|
+
console.log(formatJson(tree));
|
|
1207
|
+
});
|
|
1208
|
+
cmd.command("show <id>").description("Show collection details (use 'root' for root collection)").addHelpText("after", `
|
|
1209
|
+
Examples:
|
|
1210
|
+
$ metabase-cli collection show 5
|
|
1211
|
+
$ metabase-cli collection show root`).action(async (id) => {
|
|
1212
|
+
const client = await resolveClient();
|
|
1213
|
+
const api = new CollectionApi(client);
|
|
1214
|
+
const coll = await api.get(id === "root" ? "root" : parseInt(id));
|
|
1215
|
+
console.log(formatJson(coll));
|
|
1216
|
+
});
|
|
1217
|
+
cmd.command("items <id>").description("List items in a collection (use 'root' for root)").option("--models <models>", "Filter by type: card, dashboard, collection").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
|
|
1218
|
+
Examples:
|
|
1219
|
+
$ metabase-cli collection items 5
|
|
1220
|
+
$ metabase-cli collection items root --models card,dashboard
|
|
1221
|
+
$ metabase-cli collection items 5 --models card --format json`).action(async (id, opts) => {
|
|
1222
|
+
const client = await resolveClient();
|
|
1223
|
+
const api = new CollectionApi(client);
|
|
1224
|
+
const params = {};
|
|
1225
|
+
if (opts.models) params.models = opts.models;
|
|
1226
|
+
const collId = id === "root" ? "root" : parseInt(id);
|
|
1227
|
+
const result = await api.items(collId, params);
|
|
1228
|
+
if (opts.format === "json") {
|
|
1229
|
+
console.log(formatJson(result));
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
console.log(
|
|
1233
|
+
formatEntityTable(result.data, [
|
|
1234
|
+
{ key: "id", header: "ID" },
|
|
1235
|
+
{ key: "name", header: "Name" },
|
|
1236
|
+
{ key: "model", header: "Type" },
|
|
1237
|
+
{ key: "description", header: "Description" }
|
|
1238
|
+
])
|
|
1239
|
+
);
|
|
1240
|
+
console.log(`
|
|
1241
|
+
${result.total} item(s).`);
|
|
1242
|
+
});
|
|
1243
|
+
cmd.command("create").description("Create a new collection").requiredOption("--name <name>", "Collection name").option("--description <desc>", "Description").option("--parent <id>", "Parent collection ID", parseInt).addHelpText("after", `
|
|
1244
|
+
Examples:
|
|
1245
|
+
$ metabase-cli collection create --name "Analytics"
|
|
1246
|
+
$ metabase-cli collection create --name "Q1 Reports" --parent 3 --description "First quarter"`).action(async (opts) => {
|
|
1247
|
+
const client = await resolveClient();
|
|
1248
|
+
const api = new CollectionApi(client);
|
|
1249
|
+
const coll = await api.create({
|
|
1250
|
+
name: opts.name,
|
|
1251
|
+
description: opts.description,
|
|
1252
|
+
parent_id: opts.parent
|
|
1253
|
+
});
|
|
1254
|
+
console.log(`Collection #${coll.id} "${coll.name}" created.`);
|
|
1255
|
+
});
|
|
1256
|
+
cmd.command("update <id>").description("Update a collection (safe mode by default)").option("--name <name>", "New name").option("--description <desc>", "New description").option("--parent <id>", "Move to parent collection", parseInt).option("--unsafe", "Bypass safe mode", false).addHelpText("after", `
|
|
1257
|
+
Safe mode blocks updates to collections you didn't create. Use --unsafe to bypass.
|
|
1258
|
+
|
|
1259
|
+
Examples:
|
|
1260
|
+
$ metabase-cli collection update 5 --name "New Name"
|
|
1261
|
+
$ metabase-cli collection update 5 --parent 3 --unsafe`).action(async function(id, opts) {
|
|
1262
|
+
const client = await resolveClient();
|
|
1263
|
+
const api = new CollectionApi(client);
|
|
1264
|
+
const guard = new SafetyGuard(client, isUnsafe(this, opts.unsafe));
|
|
1265
|
+
const collId = parseInt(id);
|
|
1266
|
+
await guard.guard("collection", collId, "update", async () => {
|
|
1267
|
+
const updates = {};
|
|
1268
|
+
if (opts.name) updates.name = opts.name;
|
|
1269
|
+
if (opts.description) updates.description = opts.description;
|
|
1270
|
+
if (opts.parent !== void 0) updates.parent_id = opts.parent;
|
|
1271
|
+
const coll = await api.update(collId, updates);
|
|
1272
|
+
console.log(`Collection #${coll.id} "${coll.name}" updated.`);
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
return cmd;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// src/commands/database.ts
|
|
1279
|
+
var import_commander6 = require("commander");
|
|
1280
|
+
|
|
1281
|
+
// src/api/database.ts
|
|
1282
|
+
var DatabaseApi = class {
|
|
1283
|
+
constructor(client) {
|
|
1284
|
+
this.client = client;
|
|
1285
|
+
}
|
|
1286
|
+
async list() {
|
|
1287
|
+
return this.client.get("/api/database");
|
|
1288
|
+
}
|
|
1289
|
+
async get(id) {
|
|
1290
|
+
return this.client.get(`/api/database/${id}`);
|
|
1291
|
+
}
|
|
1292
|
+
async metadata(id) {
|
|
1293
|
+
return this.client.get(`/api/database/${id}/metadata`);
|
|
1294
|
+
}
|
|
1295
|
+
async schemas(id) {
|
|
1296
|
+
return this.client.get(`/api/database/${id}/schemas`);
|
|
1297
|
+
}
|
|
1298
|
+
async tablesInSchema(id, schema) {
|
|
1299
|
+
return this.client.get(
|
|
1300
|
+
`/api/database/${id}/schema/${encodeURIComponent(schema)}`
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
|
|
1305
|
+
// src/api/table.ts
|
|
1306
|
+
var TableApi = class {
|
|
1307
|
+
constructor(client) {
|
|
1308
|
+
this.client = client;
|
|
1309
|
+
}
|
|
1310
|
+
async get(id) {
|
|
1311
|
+
return this.client.get(`/api/table/${id}`);
|
|
1312
|
+
}
|
|
1313
|
+
async queryMetadata(id) {
|
|
1314
|
+
return this.client.get(`/api/table/${id}/query_metadata`);
|
|
1315
|
+
}
|
|
1316
|
+
async foreignKeys(id) {
|
|
1317
|
+
return this.client.get(`/api/table/${id}/fks`);
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
// src/api/field.ts
|
|
1322
|
+
var FieldApi = class {
|
|
1323
|
+
constructor(client) {
|
|
1324
|
+
this.client = client;
|
|
1325
|
+
}
|
|
1326
|
+
async get(id) {
|
|
1327
|
+
return this.client.get(`/api/field/${id}`);
|
|
1328
|
+
}
|
|
1329
|
+
async values(id) {
|
|
1330
|
+
return this.client.get(`/api/field/${id}/values`);
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
// src/commands/database.ts
|
|
1335
|
+
function databaseCommand() {
|
|
1336
|
+
const cmd = new import_commander6.Command("database").description("Browse databases, tables, and fields").addHelpText("after", `
|
|
1337
|
+
Examples:
|
|
1338
|
+
$ metabase-cli database list
|
|
1339
|
+
$ metabase-cli database show 1
|
|
1340
|
+
$ metabase-cli database schemas 1
|
|
1341
|
+
$ metabase-cli database tables 1 public`);
|
|
1342
|
+
cmd.command("list").description("List all databases").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
|
|
1343
|
+
Examples:
|
|
1344
|
+
$ metabase-cli database list
|
|
1345
|
+
$ metabase-cli database list --format json`).action(async (opts) => {
|
|
1346
|
+
const client = await resolveClient();
|
|
1347
|
+
const api = new DatabaseApi(client);
|
|
1348
|
+
const result = await api.list();
|
|
1349
|
+
if (opts.format === "json") {
|
|
1350
|
+
console.log(formatJson(result.data));
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
console.log(
|
|
1354
|
+
formatEntityTable(result.data, [
|
|
1355
|
+
{ key: "id", header: "ID" },
|
|
1356
|
+
{ key: "name", header: "Name" },
|
|
1357
|
+
{ key: "engine", header: "Engine" }
|
|
1358
|
+
])
|
|
1359
|
+
);
|
|
1360
|
+
});
|
|
1361
|
+
cmd.command("show <id>").description("Show database details").addHelpText("after", `
|
|
1362
|
+
Examples:
|
|
1363
|
+
$ metabase-cli database show 1`).action(async (id) => {
|
|
1364
|
+
const client = await resolveClient();
|
|
1365
|
+
const api = new DatabaseApi(client);
|
|
1366
|
+
const db = await api.get(parseInt(id));
|
|
1367
|
+
console.log(formatJson(db));
|
|
1368
|
+
});
|
|
1369
|
+
cmd.command("metadata <id>").description("Show database metadata (tables and fields)").addHelpText("after", `
|
|
1370
|
+
Examples:
|
|
1371
|
+
$ metabase-cli database metadata 1`).action(async (id) => {
|
|
1372
|
+
const client = await resolveClient();
|
|
1373
|
+
const api = new DatabaseApi(client);
|
|
1374
|
+
const meta = await api.metadata(parseInt(id));
|
|
1375
|
+
console.log(formatJson(meta));
|
|
1376
|
+
});
|
|
1377
|
+
cmd.command("schemas <id>").description("List schemas in a database").addHelpText("after", `
|
|
1378
|
+
Examples:
|
|
1379
|
+
$ metabase-cli database schemas 1`).action(async (id) => {
|
|
1380
|
+
const client = await resolveClient();
|
|
1381
|
+
const api = new DatabaseApi(client);
|
|
1382
|
+
const schemas = await api.schemas(parseInt(id));
|
|
1383
|
+
for (const s of schemas) console.log(s);
|
|
1384
|
+
});
|
|
1385
|
+
cmd.command("tables <dbId> <schema>").description("List tables in a database schema").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
|
|
1386
|
+
Examples:
|
|
1387
|
+
$ metabase-cli database tables 1 public
|
|
1388
|
+
$ metabase-cli database tables 1 public --format json`).action(async (dbId, schema, opts) => {
|
|
1389
|
+
const client = await resolveClient();
|
|
1390
|
+
const api = new DatabaseApi(client);
|
|
1391
|
+
const tables = await api.tablesInSchema(parseInt(dbId), schema);
|
|
1392
|
+
if (opts.format === "json") {
|
|
1393
|
+
console.log(formatJson(tables));
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
console.log(
|
|
1397
|
+
formatEntityTable(tables, [
|
|
1398
|
+
{ key: "id", header: "ID" },
|
|
1399
|
+
{ key: "name", header: "Name" },
|
|
1400
|
+
{ key: "display_name", header: "Display Name" }
|
|
1401
|
+
])
|
|
1402
|
+
);
|
|
1403
|
+
});
|
|
1404
|
+
return cmd;
|
|
1405
|
+
}
|
|
1406
|
+
function tableCommand() {
|
|
1407
|
+
const cmd = new import_commander6.Command("table").description("Inspect tables").addHelpText("after", `
|
|
1408
|
+
Examples:
|
|
1409
|
+
$ metabase-cli table show 15
|
|
1410
|
+
$ metabase-cli table metadata 15
|
|
1411
|
+
$ metabase-cli table fks 15`);
|
|
1412
|
+
cmd.command("show <id>").description("Show table details").addHelpText("after", `
|
|
1413
|
+
Examples:
|
|
1414
|
+
$ metabase-cli table show 15`).action(async (id) => {
|
|
1415
|
+
const client = await resolveClient();
|
|
1416
|
+
const api = new TableApi(client);
|
|
1417
|
+
const table = await api.get(parseInt(id));
|
|
1418
|
+
console.log(formatJson(table));
|
|
1419
|
+
});
|
|
1420
|
+
cmd.command("metadata <id>").description("Show table metadata with fields").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
|
|
1421
|
+
Examples:
|
|
1422
|
+
$ metabase-cli table metadata 15
|
|
1423
|
+
$ metabase-cli table metadata 15 --format json`).action(async (id, opts) => {
|
|
1424
|
+
const client = await resolveClient();
|
|
1425
|
+
const api = new TableApi(client);
|
|
1426
|
+
const meta = await api.queryMetadata(parseInt(id));
|
|
1427
|
+
if (opts.format === "json") {
|
|
1428
|
+
console.log(formatJson(meta));
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
console.log(`Table: ${meta.name} (${meta.display_name})`);
|
|
1432
|
+
console.log(
|
|
1433
|
+
formatEntityTable(meta.fields, [
|
|
1434
|
+
{ key: "id", header: "ID" },
|
|
1435
|
+
{ key: "name", header: "Name" },
|
|
1436
|
+
{ key: "display_name", header: "Display Name" },
|
|
1437
|
+
{ key: "base_type", header: "Type" },
|
|
1438
|
+
{ key: "semantic_type", header: "Semantic" }
|
|
1439
|
+
])
|
|
1440
|
+
);
|
|
1441
|
+
});
|
|
1442
|
+
cmd.command("fks <id>").description("Show foreign keys for a table").addHelpText("after", `
|
|
1443
|
+
Examples:
|
|
1444
|
+
$ metabase-cli table fks 15`).action(async (id) => {
|
|
1445
|
+
const client = await resolveClient();
|
|
1446
|
+
const api = new TableApi(client);
|
|
1447
|
+
const fks = await api.foreignKeys(parseInt(id));
|
|
1448
|
+
console.log(formatJson(fks));
|
|
1449
|
+
});
|
|
1450
|
+
return cmd;
|
|
1451
|
+
}
|
|
1452
|
+
function fieldCommand() {
|
|
1453
|
+
const cmd = new import_commander6.Command("field").description("Inspect fields").addHelpText("after", `
|
|
1454
|
+
Examples:
|
|
1455
|
+
$ metabase-cli field show 100
|
|
1456
|
+
$ metabase-cli field values 100`);
|
|
1457
|
+
cmd.command("show <id>").description("Show field details").addHelpText("after", `
|
|
1458
|
+
Examples:
|
|
1459
|
+
$ metabase-cli field show 100`).action(async (id) => {
|
|
1460
|
+
const client = await resolveClient();
|
|
1461
|
+
const api = new FieldApi(client);
|
|
1462
|
+
const field = await api.get(parseInt(id));
|
|
1463
|
+
console.log(formatJson(field));
|
|
1464
|
+
});
|
|
1465
|
+
cmd.command("values <id>").description("Show distinct values for a field").addHelpText("after", `
|
|
1466
|
+
Examples:
|
|
1467
|
+
$ metabase-cli field values 100`).action(async (id) => {
|
|
1468
|
+
const client = await resolveClient();
|
|
1469
|
+
const api = new FieldApi(client);
|
|
1470
|
+
const result = await api.values(parseInt(id));
|
|
1471
|
+
console.log(formatJson(result));
|
|
1472
|
+
});
|
|
1473
|
+
return cmd;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// src/commands/search.ts
|
|
1477
|
+
var import_commander7 = require("commander");
|
|
1478
|
+
|
|
1479
|
+
// src/api/search.ts
|
|
1480
|
+
var SearchApi = class {
|
|
1481
|
+
constructor(client) {
|
|
1482
|
+
this.client = client;
|
|
1483
|
+
}
|
|
1484
|
+
async search(query, params) {
|
|
1485
|
+
const qs = { q: query };
|
|
1486
|
+
if (params?.models?.length) {
|
|
1487
|
+
qs.models = params.models.join(",");
|
|
1488
|
+
}
|
|
1489
|
+
if (params?.limit !== void 0) qs.limit = String(params.limit);
|
|
1490
|
+
if (params?.offset !== void 0) qs.offset = String(params.offset);
|
|
1491
|
+
return this.client.get("/api/search", qs);
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
// src/commands/search.ts
|
|
1496
|
+
function searchCommand() {
|
|
1497
|
+
const cmd = new import_commander7.Command("search").description("Search across all entities").argument("<query>", "Search query").option("--models <models>", "Filter by type: card, dashboard, collection, table, database").option("--limit <n>", "Max results", parseInt).option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
|
|
1498
|
+
Examples:
|
|
1499
|
+
$ metabase-cli search "revenue"
|
|
1500
|
+
$ metabase-cli search "users" --models card,dashboard
|
|
1501
|
+
$ metabase-cli search "orders" --limit 5 --format json`).action(async (query, opts) => {
|
|
1502
|
+
const client = await resolveClient();
|
|
1503
|
+
const api = new SearchApi(client);
|
|
1504
|
+
const result = await api.search(query, {
|
|
1505
|
+
models: opts.models?.split(","),
|
|
1506
|
+
limit: opts.limit
|
|
1507
|
+
});
|
|
1508
|
+
if (opts.format === "json") {
|
|
1509
|
+
console.log(formatJson(result));
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
console.log(
|
|
1513
|
+
formatEntityTable(result.data, [
|
|
1514
|
+
{ key: "id", header: "ID" },
|
|
1515
|
+
{ key: "model", header: "Type" },
|
|
1516
|
+
{ key: "name", header: "Name" },
|
|
1517
|
+
{ key: "description", header: "Description" },
|
|
1518
|
+
{ key: "collection_id", header: "Collection" }
|
|
1519
|
+
])
|
|
1520
|
+
);
|
|
1521
|
+
console.log(`
|
|
1522
|
+
${result.total} result(s).`);
|
|
1523
|
+
});
|
|
1524
|
+
return cmd;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// src/commands/snippet.ts
|
|
1528
|
+
var import_commander8 = require("commander");
|
|
1529
|
+
|
|
1530
|
+
// src/api/snippet.ts
|
|
1531
|
+
var SnippetApi = class {
|
|
1532
|
+
constructor(client) {
|
|
1533
|
+
this.client = client;
|
|
1534
|
+
}
|
|
1535
|
+
async list(params) {
|
|
1536
|
+
return this.client.get("/api/native-query-snippet", params);
|
|
1537
|
+
}
|
|
1538
|
+
async get(id) {
|
|
1539
|
+
return this.client.get(`/api/native-query-snippet/${id}`);
|
|
1540
|
+
}
|
|
1541
|
+
async create(params) {
|
|
1542
|
+
return this.client.post("/api/native-query-snippet", params);
|
|
1543
|
+
}
|
|
1544
|
+
async update(id, params) {
|
|
1545
|
+
return this.client.put(`/api/native-query-snippet/${id}`, params);
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
// src/commands/snippet.ts
|
|
1550
|
+
function snippetCommand() {
|
|
1551
|
+
const cmd = new import_commander8.Command("snippet").description("Manage SQL snippets").addHelpText("after", `
|
|
1552
|
+
Examples:
|
|
1553
|
+
$ metabase-cli snippet list
|
|
1554
|
+
$ metabase-cli snippet show 3
|
|
1555
|
+
$ metabase-cli snippet create --name "Active filter" --content "WHERE active = true"
|
|
1556
|
+
$ metabase-cli snippet update 3 --content "WHERE active AND NOT deleted" --unsafe`);
|
|
1557
|
+
cmd.command("list").description("List snippets").option("--archived", "Include archived snippets").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
|
|
1558
|
+
Examples:
|
|
1559
|
+
$ metabase-cli snippet list
|
|
1560
|
+
$ metabase-cli snippet list --archived --format json`).action(async (opts) => {
|
|
1561
|
+
const client = await resolveClient();
|
|
1562
|
+
const api = new SnippetApi(client);
|
|
1563
|
+
const params = {};
|
|
1564
|
+
if (opts.archived) params.archived = "true";
|
|
1565
|
+
const snippets = await api.list(params);
|
|
1566
|
+
if (opts.format === "json") {
|
|
1567
|
+
console.log(formatJson(snippets));
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
console.log(
|
|
1571
|
+
formatEntityTable(snippets, [
|
|
1572
|
+
{ key: "id", header: "ID" },
|
|
1573
|
+
{ key: "name", header: "Name" },
|
|
1574
|
+
{ key: "description", header: "Description" },
|
|
1575
|
+
{ key: "creator_id", header: "Creator" }
|
|
1576
|
+
])
|
|
1577
|
+
);
|
|
1578
|
+
});
|
|
1579
|
+
cmd.command("show <id>").description("Show snippet details and content").addHelpText("after", `
|
|
1580
|
+
Examples:
|
|
1581
|
+
$ metabase-cli snippet show 3`).action(async (id) => {
|
|
1582
|
+
const client = await resolveClient();
|
|
1583
|
+
const api = new SnippetApi(client);
|
|
1584
|
+
const snippet = await api.get(parseInt(id));
|
|
1585
|
+
console.log(formatJson(snippet));
|
|
1586
|
+
});
|
|
1587
|
+
cmd.command("create").description("Create a new snippet").requiredOption("--name <name>", "Snippet name").requiredOption("--content <sql>", "SQL content").option("--description <desc>", "Description").option("--collection <id>", "Collection ID", parseInt).addHelpText("after", `
|
|
1588
|
+
Examples:
|
|
1589
|
+
$ metabase-cli snippet create --name "Active filter" --content "WHERE active = true"
|
|
1590
|
+
$ metabase-cli snippet create --name "Date range" --content "WHERE created_at > NOW() - INTERVAL '30 days'"`).action(async (opts) => {
|
|
1591
|
+
const client = await resolveClient();
|
|
1592
|
+
const api = new SnippetApi(client);
|
|
1593
|
+
const snippet = await api.create({
|
|
1594
|
+
name: opts.name,
|
|
1595
|
+
content: opts.content,
|
|
1596
|
+
description: opts.description,
|
|
1597
|
+
collection_id: opts.collection
|
|
1598
|
+
});
|
|
1599
|
+
console.log(`Snippet #${snippet.id} "${snippet.name}" created.`);
|
|
1600
|
+
});
|
|
1601
|
+
cmd.command("update <id>").description("Update a snippet (safe mode by default)").option("--name <name>", "New name").option("--content <sql>", "New SQL content").option("--description <desc>", "New description").option("--unsafe", "Bypass safe mode", false).addHelpText("after", `
|
|
1602
|
+
Safe mode blocks updates to snippets you didn't create. Use --unsafe to bypass.
|
|
1603
|
+
|
|
1604
|
+
Examples:
|
|
1605
|
+
$ metabase-cli snippet update 3 --content "WHERE active = true AND deleted_at IS NULL"
|
|
1606
|
+
$ metabase-cli snippet update 3 --name "New Name" --unsafe`).action(async function(id, opts) {
|
|
1607
|
+
const client = await resolveClient();
|
|
1608
|
+
const api = new SnippetApi(client);
|
|
1609
|
+
const guard = new SafetyGuard(client, isUnsafe(this, opts.unsafe));
|
|
1610
|
+
const snippetId = parseInt(id);
|
|
1611
|
+
await guard.guard("snippet", snippetId, "update", async () => {
|
|
1612
|
+
const updates = {};
|
|
1613
|
+
if (opts.name) updates.name = opts.name;
|
|
1614
|
+
if (opts.content) updates.content = opts.content;
|
|
1615
|
+
if (opts.description) updates.description = opts.description;
|
|
1616
|
+
const snippet = await api.update(snippetId, updates);
|
|
1617
|
+
console.log(`Snippet #${snippet.id} "${snippet.name}" updated.`);
|
|
1618
|
+
});
|
|
1619
|
+
});
|
|
1620
|
+
return cmd;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// src/utils/errors.ts
|
|
1624
|
+
function handleError(err) {
|
|
1625
|
+
if (err instanceof Error) {
|
|
1626
|
+
console.error(`Error: ${err.message}`);
|
|
1627
|
+
} else {
|
|
1628
|
+
console.error("Unknown error:", err);
|
|
1629
|
+
}
|
|
1630
|
+
process.exit(1);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// bin/metabase.ts
|
|
1634
|
+
var pkg = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path3.resolve)(__dirname, "..", "package.json"), "utf-8"));
|
|
1635
|
+
var program = new import_commander9.Command();
|
|
1636
|
+
program.name("metabase-cli").description("Headless CLI for Metabase instances").version(pkg.version).option("--unsafe", "Bypass safe mode globally");
|
|
1637
|
+
program.addCommand(profileCommand());
|
|
1638
|
+
program.addCommand(queryCommand());
|
|
1639
|
+
program.addCommand(questionCommand());
|
|
1640
|
+
program.addCommand(dashboardCommand());
|
|
1641
|
+
program.addCommand(collectionCommand());
|
|
1642
|
+
program.addCommand(databaseCommand());
|
|
1643
|
+
program.addCommand(tableCommand());
|
|
1644
|
+
program.addCommand(fieldCommand());
|
|
1645
|
+
program.addCommand(searchCommand());
|
|
1646
|
+
program.addCommand(snippetCommand());
|
|
1647
|
+
program.command("login").description("Login to the active profile").addHelpText("after", `
|
|
1648
|
+
Examples:
|
|
1649
|
+
$ metabase-cli login`).action(async () => {
|
|
1650
|
+
const profile = getActiveProfile();
|
|
1651
|
+
if (!profile) {
|
|
1652
|
+
console.error("No active profile. Run: metabase-cli profile add <name>");
|
|
1653
|
+
process.exit(1);
|
|
1654
|
+
}
|
|
1655
|
+
if (profile.auth.method !== "session") {
|
|
1656
|
+
console.log("Profile uses API key auth \u2014 no login needed.");
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
const client = new MetabaseClient(profile);
|
|
1660
|
+
await client.login();
|
|
1661
|
+
console.log(`Logged in to ${profile.domain} as ${profile.auth.email}.`);
|
|
1662
|
+
if (client.getProfile().user) {
|
|
1663
|
+
console.log(`User: ${client.getProfile().user.first_name} ${client.getProfile().user.last_name} (ID: ${client.getProfile().user.id})`);
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
program.command("logout").description("Logout from the active profile").addHelpText("after", `
|
|
1667
|
+
Examples:
|
|
1668
|
+
$ metabase-cli logout`).action(async () => {
|
|
1669
|
+
const profile = getActiveProfile();
|
|
1670
|
+
if (!profile) {
|
|
1671
|
+
console.error("No active profile.");
|
|
1672
|
+
process.exit(1);
|
|
1673
|
+
}
|
|
1674
|
+
const client = new MetabaseClient(profile);
|
|
1675
|
+
await client.logout();
|
|
1676
|
+
console.log("Logged out.");
|
|
1677
|
+
});
|
|
1678
|
+
program.command("whoami").description("Show current user info").option("--refresh", "Refresh cached user info from server").addHelpText("after", `
|
|
1679
|
+
Examples:
|
|
1680
|
+
$ metabase-cli whoami
|
|
1681
|
+
$ metabase-cli whoami --refresh`).action(async (opts) => {
|
|
1682
|
+
const profile = getActiveProfile();
|
|
1683
|
+
if (!profile) {
|
|
1684
|
+
console.error("No active profile.");
|
|
1685
|
+
process.exit(1);
|
|
1686
|
+
}
|
|
1687
|
+
if (!opts.refresh && profile.user) {
|
|
1688
|
+
console.log(`${profile.user.first_name} ${profile.user.last_name}`);
|
|
1689
|
+
console.log(`Email: ${profile.user.email}`);
|
|
1690
|
+
console.log(`User ID: ${profile.user.id}`);
|
|
1691
|
+
console.log(`Superuser: ${profile.user.is_superuser}`);
|
|
1692
|
+
console.log(`Profile: ${profile.name}`);
|
|
1693
|
+
console.log(`Domain: ${profile.domain}`);
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
const client = new MetabaseClient(profile);
|
|
1697
|
+
await client.ensureAuthenticated();
|
|
1698
|
+
const user = await client.get("/api/user/current");
|
|
1699
|
+
updateProfile(profile.name, {
|
|
1700
|
+
user: {
|
|
1701
|
+
id: user.id,
|
|
1702
|
+
email: user.email,
|
|
1703
|
+
first_name: user.first_name,
|
|
1704
|
+
last_name: user.last_name,
|
|
1705
|
+
is_superuser: user.is_superuser
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1708
|
+
console.log(formatJson(user));
|
|
1709
|
+
});
|
|
1710
|
+
program.parseAsync(process.argv).catch(handleError);
|