@deepsql/mcp 0.3.0 → 0.6.0

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.
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+
3
+ const test = require("node:test");
4
+ const assert = require("node:assert/strict");
5
+
6
+ const { parseArgs, buildOpts } = require("../cli");
7
+
8
+ function opts(argv) {
9
+ return buildOpts(parseArgs(argv));
10
+ }
11
+
12
+ // users -----------------------------------------------------------------------
13
+
14
+ test("users add positional email + role/name flags", () => {
15
+ const o = opts(["add", "x@y.com", "--role", "DEVELOPER", "--name", "X"]);
16
+ assert.deepEqual(o.positional, ["add", "x@y.com"]);
17
+ assert.equal(o.role, "DEVELOPER");
18
+ assert.equal(o.name, "X");
19
+ });
20
+
21
+ test("users delete --yes flag", () => {
22
+ const o = opts(["delete", "alice@x.com", "--yes"]);
23
+ assert.equal(o.yes, true);
24
+ assert.deepEqual(o.positional, ["delete", "alice@x.com"]);
25
+ });
26
+
27
+ test("users reset-password --password-stdin flag", () => {
28
+ const o = opts(["reset-password", "alice@x.com", "--password-stdin"]);
29
+ assert.equal(o.passwordStdin, true);
30
+ assert.equal(o.password, null); // stays null since it's a separate flag
31
+ });
32
+
33
+ // access ----------------------------------------------------------------------
34
+
35
+ test("access grant requires user/connection/level", () => {
36
+ const o = opts([
37
+ "grant",
38
+ "--user",
39
+ "alice@x.com",
40
+ "--connection",
41
+ "mylocalpg",
42
+ "--level",
43
+ "read",
44
+ ]);
45
+ assert.equal(o.user, "alice@x.com");
46
+ assert.equal(o.connection, "mylocalpg");
47
+ assert.equal(o.level, "read");
48
+ });
49
+
50
+ test("access policy passes positional user + connection", () => {
51
+ const o = opts(["policy", "alice@x.com", "mylocalpg"]);
52
+ assert.deepEqual(o.positional, ["policy", "alice@x.com", "mylocalpg"]);
53
+ });
54
+
55
+ // permissions -----------------------------------------------------------------
56
+
57
+ test("permissions override --grant + --reason", () => {
58
+ const o = opts([
59
+ "override",
60
+ "--role",
61
+ "DEVELOPER",
62
+ "--permission",
63
+ "USE_CHAT",
64
+ "--grant",
65
+ "--reason",
66
+ "Beta access",
67
+ ]);
68
+ assert.equal(o.role, "DEVELOPER");
69
+ assert.equal(o.permission, "USE_CHAT");
70
+ assert.equal(o.grant, true);
71
+ assert.equal(o.revoke, false);
72
+ assert.equal(o.reason, "Beta access");
73
+ });
74
+
75
+ test("permissions override --revoke flips to revoke", () => {
76
+ const o = opts(["override", "--role", "DEVELOPER", "--permission", "USE_CHAT", "--revoke"]);
77
+ assert.equal(o.grant, false);
78
+ assert.equal(o.revoke, true);
79
+ });
80
+
81
+ // slow-queries ----------------------------------------------------------------
82
+
83
+ test("slow-queries history N positional", () => {
84
+ const o = opts(["history", "--connection", "mylocalpg", "5"]);
85
+ assert.deepEqual(o.positional, ["history", "5"]);
86
+ assert.equal(o.connection, "mylocalpg");
87
+ });
88
+
89
+ test("slow-queries analyze flags", () => {
90
+ const o = opts([
91
+ "analyze",
92
+ "--connection",
93
+ "mylocalpg",
94
+ "--time-range",
95
+ "LAST_HOUR",
96
+ "--threshold-ms",
97
+ "200",
98
+ "--limit",
99
+ "20",
100
+ ]);
101
+ assert.equal(o.timeRange, "LAST_HOUR");
102
+ assert.equal(o.thresholdMs, "200");
103
+ assert.equal(o.limit, "20");
104
+ });
105
+
106
+ test("slow-queries optimize uses --query-id", () => {
107
+ const o = opts(["optimize", "--connection", "mylocalpg", "--query-id", "q-123"]);
108
+ assert.equal(o.queryId, "q-123");
109
+ });
110
+
111
+ test("slow-queries delete --history-id with --yes", () => {
112
+ const o = opts(["delete", "--history-id", "42", "--yes"]);
113
+ assert.equal(o.historyId, "42");
114
+ assert.equal(o.yes, true);
115
+ });
116
+
117
+ // setup -----------------------------------------------------------------------
118
+
119
+ test("setup --force --skip-email", () => {
120
+ const o = opts(["--force", "--skip-email"]);
121
+ assert.equal(o.force, true);
122
+ assert.equal(o.skipEmail, true);
123
+ });
124
+
125
+ // password flag stays a flag for login + value for users -----------------------
126
+
127
+ test("--password alone (login flow) leaves password truthy", () => {
128
+ const o = opts(["--password"]);
129
+ assert.equal(!!o.password, true);
130
+ });
131
+
132
+ test("--password=secret keeps the string value (still truthy)", () => {
133
+ const o = opts(["--password=secret"]);
134
+ assert.equal(o.password, "secret");
135
+ });
@@ -3,13 +3,15 @@
3
3
  const os = require("node:os");
4
4
  const { request } = require("../api/client");
5
5
  const { resolveSession } = require("./_session");
6
+ const { resolveConnectionId } = require("./_connections");
6
7
 
7
8
  async function run(opts, { stdout = process.stdout } = {}) {
8
9
  const question = opts.positional.join(" ").trim();
9
- if (!question) throw new Error("Pass a question: `deepsql ask \"why is this query slow?\" --connection <id>`.");
10
- if (!opts.connection) throw new Error("--connection <id> is required.");
10
+ if (!question) throw new Error("Pass a question: `deepsql ask \"why is this query slow?\" --connection <name>`.");
11
+ if (!opts.connection) throw new Error("--connection <name> is required.");
11
12
 
12
13
  const session = resolveSession(opts);
14
+ const connectionId = await resolveConnectionId(session, opts.connection);
13
15
  const userId = opts.user || `cli-${os.userInfo().username}`;
14
16
  const projectId = opts.project || "deepsql-cli";
15
17
 
@@ -18,7 +20,7 @@ async function run(opts, { stdout = process.stdout } = {}) {
18
20
  token: session.token,
19
21
  timeoutMs: 240000,
20
22
  json: {
21
- connectionId: opts.connection,
23
+ connectionId,
22
24
  message: question,
23
25
  chatId: opts.chat || null,
24
26
  userId,
@@ -18,11 +18,23 @@ async function run(opts, { stdout = process.stdout } = {}) {
18
18
  stdout.write("No connections.\n");
19
19
  return;
20
20
  }
21
- for (const conn of data) {
22
- const id = conn.id || conn.connectionId;
23
- const name = conn.connectionName || conn.name || "(unnamed)";
24
- const dbType = conn.databaseType || conn.dbType || "?";
25
- stdout.write(`${id} ${dbType.padEnd(10)} ${name}\n`);
21
+ // Name first it's what users will pass to --connection. ID kept on the
22
+ // right for back-compat / scripting.
23
+ const rows = data.map((conn) => ({
24
+ name: conn.connectionName || conn.name || "(unnamed)",
25
+ type: conn.databaseType || conn.dbType || "?",
26
+ id: conn.id || conn.connectionId || "",
27
+ }));
28
+ const widths = {
29
+ name: Math.max(4, ...rows.map((r) => r.name.length)),
30
+ type: Math.max(4, ...rows.map((r) => r.type.length)),
31
+ };
32
+ stdout.write(
33
+ `${"NAME".padEnd(widths.name)} ${"TYPE".padEnd(widths.type)} ID\n` +
34
+ `${"-".repeat(widths.name)} ${"-".repeat(widths.type)} ${"-".repeat(36)}\n`,
35
+ );
36
+ for (const row of rows) {
37
+ stdout.write(`${row.name.padEnd(widths.name)} ${row.type.padEnd(widths.type)} ${row.id}\n`);
26
38
  }
27
39
  }
28
40
 
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * `deepsql digest` — surface the daily DeepSQL digest from the terminal.
5
+ *
6
+ * deepsql digest → show the single most recent digest, full body
7
+ * deepsql digest <N> → list the last N digests (compact, one per row)
8
+ * deepsql digest list [--count N] → same as above; explicit form
9
+ * deepsql digest show <id> → show a specific digest by id
10
+ * --connection <id> → filter to one connection
11
+ * --json → raw JSON output
12
+ *
13
+ * Backend: GET /admin/slack/digests?connectionId=&page=0&size=N
14
+ * Returns a Spring Data Page<SlackDigestLog>: { content: [...], totalElements, ... }
15
+ * Requires ADMIN role on the calling user's MCP token.
16
+ */
17
+
18
+ const { ApiError, request } = require("../api/client");
19
+ const { resolveSession } = require("./_session");
20
+ const { resolveConnectionId } = require("./_connections");
21
+
22
+ const DEFAULT_LIST_COUNT = 10;
23
+ const MAX_COUNT = 100;
24
+
25
+ async function run(opts, { stdout = process.stdout, stderr = process.stderr } = {}) {
26
+ if (!opts.connection) {
27
+ throw new Error(
28
+ "--connection <name> is required. Digests are per-connection — pick one from `deepsql connections list`.",
29
+ );
30
+ }
31
+ const session = resolveSession(opts);
32
+ const connectionId = await resolveConnectionId(session, opts.connection);
33
+ const sub = opts.positional[0];
34
+
35
+ // `deepsql digest <N>` shorthand — first positional is a number.
36
+ if (sub && /^\d+$/.test(sub)) {
37
+ return runList(session, connectionId, parseCount(sub), opts, { stdout, stderr });
38
+ }
39
+ if (!sub || sub === "latest") {
40
+ return runLatest(session, connectionId, opts, { stdout, stderr });
41
+ }
42
+ if (sub === "list") {
43
+ const count = parseCount(opts.count) || parseCount(opts.positional[1]) || DEFAULT_LIST_COUNT;
44
+ return runList(session, connectionId, count, opts, { stdout, stderr });
45
+ }
46
+ if (sub === "show") {
47
+ const id = opts.positional[1];
48
+ if (!id) throw new Error("Pass the digest id: `deepsql digest show <id> --connection <name>`.");
49
+ return runShow(session, connectionId, id, opts, { stdout, stderr });
50
+ }
51
+ throw new Error(`Unknown digest subcommand: ${sub}. Try \`latest\`, \`list\`, \`show <id>\`, or pass a number.`);
52
+ }
53
+
54
+ async function runLatest(session, connectionId, opts, { stdout }) {
55
+ const page = await fetchPage(session, connectionId, 0, 1);
56
+ const digest = page.content?.[0];
57
+ if (!digest) {
58
+ stdout.write(`No digests yet for connection "${opts.connection}".\n`);
59
+ return;
60
+ }
61
+ if (opts.json) {
62
+ stdout.write(`${JSON.stringify(digest, null, 2)}\n`);
63
+ return;
64
+ }
65
+ printFull(stdout, digest);
66
+ }
67
+
68
+ async function runList(session, connectionId, requestedCount, opts, { stdout }) {
69
+ const count = Math.min(requestedCount, MAX_COUNT);
70
+ const page = await fetchPage(session, connectionId, 0, count);
71
+ const items = page.content || [];
72
+ if (opts.json) {
73
+ stdout.write(`${JSON.stringify(items, null, 2)}\n`);
74
+ return;
75
+ }
76
+ if (items.length === 0) {
77
+ stdout.write(`No digests yet for connection "${opts.connection}".\n`);
78
+ return;
79
+ }
80
+ printTable(stdout, items);
81
+ if (page.totalElements && page.totalElements > items.length) {
82
+ stdout.write(`\n${items.length} of ${page.totalElements} shown — pass a larger N to see more.\n`);
83
+ }
84
+ }
85
+
86
+ async function runShow(session, connectionId, id, opts, { stdout }) {
87
+ // No single-digest backend endpoint exists yet, so locate by paging.
88
+ // Cheap enough for typical digest counts; we cap at a few pages.
89
+ const target = String(id);
90
+ for (let page = 0; page < 10; page++) {
91
+ const result = await fetchPage(session, connectionId, page, 50);
92
+ const hit = (result.content || []).find((d) => String(d.id) === target);
93
+ if (hit) {
94
+ if (opts.json) {
95
+ stdout.write(`${JSON.stringify(hit, null, 2)}\n`);
96
+ return;
97
+ }
98
+ printFull(stdout, hit);
99
+ return;
100
+ }
101
+ if (result.last || (result.content || []).length === 0) break;
102
+ }
103
+ throw new Error(`Digest ${id} not found in the most recent 500 entries for "${opts.connection}".`);
104
+ }
105
+
106
+ async function fetchPage(session, connectionId, page, size) {
107
+ try {
108
+ return await request(session.baseUrl, "/admin/slack/digests", {
109
+ token: session.token,
110
+ query: {
111
+ connectionId,
112
+ page,
113
+ size,
114
+ },
115
+ });
116
+ } catch (err) {
117
+ if (err instanceof ApiError && err.status === 403) {
118
+ throw new Error(
119
+ "Access denied — fetching digests requires ADMIN role. Ask an administrator to mint a token, or run `deepsql login` as an admin.",
120
+ );
121
+ }
122
+ throw err;
123
+ }
124
+ }
125
+
126
+ function printTable(stdout, items) {
127
+ const rows = items.map((d) => ({
128
+ id: String(d.id ?? ""),
129
+ sentAt: formatTimestamp(d.sentAt),
130
+ status: d.status || "?",
131
+ connection: trim(d.connectionName || d.connectionId || "—", 24),
132
+ headline: trim(d.headline || firstLine(d.content) || "(no headline)", 60),
133
+ }));
134
+ const cols = [
135
+ { key: "id", label: "ID" },
136
+ { key: "sentAt", label: "Sent" },
137
+ { key: "status", label: "Status" },
138
+ { key: "connection", label: "Connection" },
139
+ { key: "headline", label: "Headline" },
140
+ ];
141
+ const widths = cols.map((c) => Math.max(c.label.length, ...rows.map((r) => r[c.key].length)));
142
+ const header = cols.map((c, i) => c.label.padEnd(widths[i])).join(" ");
143
+ const sep = widths.map((w) => "-".repeat(w)).join(" ");
144
+ stdout.write(`${header}\n${sep}\n`);
145
+ for (const row of rows) {
146
+ stdout.write(`${cols.map((c, i) => row[c.key].padEnd(widths[i])).join(" ")}\n`);
147
+ }
148
+ }
149
+
150
+ function printFull(stdout, d) {
151
+ const sent = formatTimestamp(d.sentAt);
152
+ stdout.write(`Digest #${d.id} · ${sent} · ${d.status || "?"}\n`);
153
+ if (d.connectionName || d.connectionId) {
154
+ stdout.write(`Connection: ${d.connectionName || d.connectionId}\n`);
155
+ }
156
+ if (d.headline) {
157
+ stdout.write(`Headline: ${d.headline}\n`);
158
+ }
159
+ stdout.write("\n");
160
+ if (d.status === "FAILED" && d.errorMessage) {
161
+ stdout.write(`Error: ${d.errorMessage}\n`);
162
+ return;
163
+ }
164
+ stdout.write(`${d.content || "(empty)"}\n`);
165
+ }
166
+
167
+ function parseCount(value) {
168
+ if (value == null) return null;
169
+ const n = Number.parseInt(value, 10);
170
+ if (!Number.isFinite(n) || n <= 0) return null;
171
+ return n;
172
+ }
173
+
174
+ function formatTimestamp(value) {
175
+ if (!value) return "—";
176
+ try {
177
+ const d = new Date(value);
178
+ if (Number.isNaN(d.getTime())) return String(value);
179
+ return d.toISOString().replace("T", " ").slice(0, 16) + "Z";
180
+ } catch {
181
+ return String(value);
182
+ }
183
+ }
184
+
185
+ function trim(text, maxLen) {
186
+ if (!text) return "";
187
+ const s = String(text).replace(/\s+/g, " ").trim();
188
+ return s.length > maxLen ? s.slice(0, maxLen - 1) + "…" : s;
189
+ }
190
+
191
+ function firstLine(text) {
192
+ if (!text) return "";
193
+ const idx = text.indexOf("\n");
194
+ return idx === -1 ? text : text.slice(0, idx);
195
+ }
196
+
197
+ module.exports = { run };
@@ -4,19 +4,21 @@ const fs = require("node:fs");
4
4
  const { request } = require("../api/client");
5
5
  const { validateReadOnlySql } = require("../../deepsql-phase1-lib");
6
6
  const { resolveSession } = require("./_session");
7
+ const { resolveConnectionId } = require("./_connections");
7
8
 
8
9
  async function run(opts, { stdout = process.stdout } = {}) {
9
- if (!opts.connection) throw new Error("--connection <id> is required.");
10
+ if (!opts.connection) throw new Error("--connection <name> is required.");
10
11
  const sql = readSqlInput(opts);
11
12
  // EXPLAIN ANALYZE is mutating; require plain EXPLAIN.
12
13
  const validation = validateReadOnlySql(sql, { allowExplain: false });
13
14
  if (!validation.ok) throw new Error(validation.reason);
14
15
 
15
16
  const session = resolveSession(opts);
17
+ const connectionId = await resolveConnectionId(session, opts.connection);
16
18
  const response = await request(session.baseUrl, "/mcp/explain-readonly", {
17
19
  method: "POST",
18
20
  token: session.token,
19
- json: { connectionId: opts.connection, query: validation.normalizedQuery },
21
+ json: { connectionId, query: validation.normalizedQuery },
20
22
  });
21
23
 
22
24
  if (opts.json) {
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * `deepsql permissions` — global role-based permission overrides.
5
+ *
6
+ * deepsql permissions list [--role <r>] [--json]
7
+ * deepsql permissions override --role <r> --permission <p> --grant|--revoke [--reason "..."]
8
+ * deepsql permissions reset --role <r> --permission <p>
9
+ *
10
+ * Backed by /permissions/** (overrides require ROLE_ADMIN).
11
+ */
12
+
13
+ const { ApiError, request } = require("../api/client");
14
+ const { resolveSession } = require("./_session");
15
+
16
+ const SUBCOMMANDS = {
17
+ list: cmdList,
18
+ override: cmdOverride,
19
+ reset: cmdReset,
20
+ };
21
+
22
+ async function run(opts, io = {}) {
23
+ const sub = opts.positional[0];
24
+ if (!sub) throw new Error("Usage: deepsql permissions <list|override|reset> ...");
25
+ const handler = SUBCOMMANDS[sub];
26
+ if (!handler) throw new Error(`Unknown permissions subcommand: ${sub}.`);
27
+ return wrap(handler)(
28
+ { ...opts, positional: opts.positional.slice(1) },
29
+ io,
30
+ );
31
+ }
32
+
33
+ function wrap(handler) {
34
+ return async (opts, io) => {
35
+ try {
36
+ return await handler(opts, io);
37
+ } catch (err) {
38
+ if (err instanceof ApiError && err.status === 403) {
39
+ throw new Error("Access denied — managing permissions requires ADMIN role.");
40
+ }
41
+ throw err;
42
+ }
43
+ };
44
+ }
45
+
46
+ // ─── list ──────────────────────────────────────────────────────────────────
47
+
48
+ async function cmdList(opts, { stdout = process.stdout } = {}) {
49
+ const session = resolveSession(opts);
50
+ const [registry, roles, overrides] = await Promise.all([
51
+ request(session.baseUrl, "/permissions/registry", { token: session.token }),
52
+ request(session.baseUrl, "/permissions/roles", { token: session.token }),
53
+ request(session.baseUrl, "/permissions/overrides", { token: session.token }),
54
+ ]);
55
+
56
+ if (opts.json) {
57
+ stdout.write(`${JSON.stringify({ registry, roles, overrides }, null, 2)}\n`);
58
+ return;
59
+ }
60
+
61
+ const roleList = Array.isArray(roles) ? roles : roles?.roles || [];
62
+ const overrideList = Array.isArray(overrides) ? overrides : overrides?.overrides || [];
63
+
64
+ if (opts.role) {
65
+ const wanted = String(opts.role).toUpperCase();
66
+ const role = roleList.find(
67
+ (r) => (r.code || r.role || r.name || "").toUpperCase() === wanted,
68
+ );
69
+ if (!role) {
70
+ const available = roleList.map((r) => r.code || r.role || r.name).filter(Boolean).join(", ");
71
+ throw new Error(`Role "${opts.role}" not found. Available: ${available}.`);
72
+ }
73
+ const code = role.code || role.role || role.name;
74
+ stdout.write(`${code} permissions (effective):\n`);
75
+ const perms = role.effectivePermissions || role.permissions || [];
76
+ for (const p of perms) {
77
+ stdout.write(` - ${typeof p === "string" ? p : p.code || p.name}\n`);
78
+ }
79
+ return;
80
+ }
81
+
82
+ if (overrideList.length === 0) {
83
+ stdout.write("No active overrides — every role has its default permissions.\n");
84
+ } else {
85
+ stdout.write("Active overrides:\n");
86
+ for (const o of overrideList) {
87
+ const role = o.role || "";
88
+ const perm = o.permissionCode || o.permission || "";
89
+ const granted = o.granted ? "GRANT" : "REVOKE";
90
+ const reason = o.reason ? ` — ${o.reason}` : "";
91
+ stdout.write(` ${role.padEnd(12)} ${granted.padEnd(7)} ${perm}${reason}\n`);
92
+ }
93
+ }
94
+
95
+ if (roleList.length > 0) {
96
+ stdout.write("\nRoles:\n");
97
+ for (const r of roleList) {
98
+ const name = r.code || r.role || r.name || "?";
99
+ const count = (r.effectivePermissions || r.permissions || []).length;
100
+ stdout.write(` ${name.padEnd(12)} ${count} permission(s)\n`);
101
+ }
102
+ stdout.write("\nUse `--role <NAME>` to see one role's full permission list.\n");
103
+ }
104
+ }
105
+
106
+ // ─── override ──────────────────────────────────────────────────────────────
107
+
108
+ async function cmdOverride(opts, { stdout = process.stdout } = {}) {
109
+ if (!opts.role) throw new Error("--role <ROLE> is required.");
110
+ if (!opts.permission) throw new Error("--permission <PERMISSION> is required.");
111
+ if (!opts.grant && !opts.revoke) {
112
+ throw new Error("Pass --grant or --revoke.");
113
+ }
114
+ if (opts.grant && opts.revoke) {
115
+ throw new Error("--grant and --revoke are mutually exclusive.");
116
+ }
117
+ const granted = !!opts.grant;
118
+ const session = resolveSession(opts);
119
+ const result = await request(session.baseUrl, "/permissions/overrides", {
120
+ method: "POST",
121
+ token: session.token,
122
+ json: {
123
+ role: String(opts.role).toUpperCase(),
124
+ permission: String(opts.permission).toUpperCase(),
125
+ granted,
126
+ reason: opts.reason || null,
127
+ },
128
+ });
129
+ stdout.write(`${result?.message || "Override applied."}\n`);
130
+ }
131
+
132
+ // ─── reset ─────────────────────────────────────────────────────────────────
133
+
134
+ async function cmdReset(opts, { stdout = process.stdout } = {}) {
135
+ if (!opts.role) throw new Error("--role <ROLE> is required.");
136
+ if (!opts.permission) throw new Error("--permission <PERMISSION> is required.");
137
+ const session = resolveSession(opts);
138
+ const result = await request(session.baseUrl, "/permissions/overrides", {
139
+ method: "DELETE",
140
+ token: session.token,
141
+ json: {
142
+ role: String(opts.role).toUpperCase(),
143
+ permission: String(opts.permission).toUpperCase(),
144
+ },
145
+ });
146
+ stdout.write(`${result?.message || "Override removed."}\n`);
147
+ }
148
+
149
+ module.exports = { run };
@@ -4,14 +4,16 @@ const fs = require("node:fs");
4
4
  const { request } = require("../api/client");
5
5
  const { validateReadOnlySql } = require("../../deepsql-phase1-lib");
6
6
  const { resolveSession } = require("./_session");
7
+ const { resolveConnectionId } = require("./_connections");
7
8
 
8
9
  async function run(opts, { stdout = process.stdout } = {}) {
9
- if (!opts.connection) throw new Error("--connection <id> is required.");
10
+ if (!opts.connection) throw new Error("--connection <name> is required.");
10
11
  const sql = readSqlInput(opts);
11
12
  const validation = validateReadOnlySql(sql, { allowExplain: true });
12
13
  if (!validation.ok) throw new Error(validation.reason);
13
14
 
14
15
  const session = resolveSession(opts);
16
+ const connectionId = await resolveConnectionId(session, opts.connection);
15
17
  const limit = clampInt(opts.limit, 1, 1000, 100);
16
18
  const timeout = opts.timeoutSeconds == null ? null : clampInt(opts.timeoutSeconds, 1, 60, null);
17
19
 
@@ -19,7 +21,7 @@ async function run(opts, { stdout = process.stdout } = {}) {
19
21
  method: "POST",
20
22
  token: session.token,
21
23
  json: {
22
- connectionId: opts.connection,
24
+ connectionId,
23
25
  query: validation.normalizedQuery,
24
26
  limit,
25
27
  timeoutSeconds: timeout,
@@ -48,19 +50,49 @@ function clampInt(value, min, max, fallback) {
48
50
  }
49
51
 
50
52
  function printRows(stdout, response) {
51
- const rows = response?.rows || response?.data || [];
52
- const columns = response?.columns || (rows[0] ? Object.keys(rows[0]) : []);
53
- if (columns.length === 0) {
53
+ // Backend returns: { result: { columns: [...], rows: [[v1,v2,...], ...],
54
+ // rowCount, totalRowCount, isLimited, ... },
55
+ // success, queryType }
56
+ // Tolerate older shapes too — `rows` directly on the response, with rows as
57
+ // either arrays or row-objects.
58
+ const result = response?.result ?? response ?? {};
59
+ const rawRows = result.rows ?? result.data ?? [];
60
+ let columns = result.columns;
61
+ if (!Array.isArray(columns) || columns.length === 0) {
62
+ columns = rawRows[0] && !Array.isArray(rawRows[0]) ? Object.keys(rawRows[0]) : [];
63
+ }
64
+ if (columns.length === 0 || rawRows.length === 0) {
54
65
  stdout.write("(no rows)\n");
55
66
  return;
56
67
  }
57
- const widths = columns.map((c) => Math.max(c.length, ...rows.map((r) => String(r[c] ?? "").length)));
68
+ const cellAt = (row, idx, col) =>
69
+ Array.isArray(row) ? row[idx] : row?.[col];
70
+ const widths = columns.map((c, i) =>
71
+ Math.max(
72
+ String(c).length,
73
+ ...rawRows.map((r) => String(cellAt(r, i, c) ?? "").length),
74
+ ),
75
+ );
58
76
  const sep = widths.map((w) => "-".repeat(w)).join(" ");
59
- stdout.write(`${columns.map((c, i) => c.padEnd(widths[i])).join(" ")}\n${sep}\n`);
60
- for (const row of rows) {
61
- stdout.write(`${columns.map((c, i) => String(row[c] ?? "").padEnd(widths[i])).join(" ")}\n`);
77
+ stdout.write(
78
+ `${columns.map((c, i) => String(c).padEnd(widths[i])).join(" ")}\n${sep}\n`,
79
+ );
80
+ for (const row of rawRows) {
81
+ stdout.write(
82
+ `${columns
83
+ .map((c, i) => String(cellAt(row, i, c) ?? "").padEnd(widths[i]))
84
+ .join(" ")}\n`,
85
+ );
86
+ }
87
+ if (result.isLimited || result.truncated) {
88
+ const shown = rawRows.length;
89
+ const total = result.totalRowCount;
90
+ stdout.write(
91
+ total != null && total > shown
92
+ ? `(showing ${shown} of ${total} rows)\n`
93
+ : `(result limited to ${shown} rows)\n`,
94
+ );
62
95
  }
63
- if (response?.truncated) stdout.write(`(truncated to ${rows.length} rows)\n`);
64
96
  }
65
97
 
66
98
  module.exports = { run };
@@ -2,15 +2,17 @@
2
2
 
3
3
  const { request } = require("../api/client");
4
4
  const { resolveSession } = require("./_session");
5
+ const { resolveConnectionId } = require("./_connections");
5
6
 
6
7
  async function run(opts, { stdout = process.stdout } = {}) {
7
- if (!opts.connection) throw new Error("--connection <id> is required.");
8
+ if (!opts.connection) throw new Error("--connection <name> is required.");
8
9
  const session = resolveSession(opts);
10
+ const connectionId = await resolveConnectionId(session, opts.connection);
9
11
  const sub = opts.positional[0] || "tables";
10
12
  const path =
11
13
  sub === "objects"
12
- ? `/connections/${encodeURIComponent(opts.connection)}/objects`
13
- : `/connections/${encodeURIComponent(opts.connection)}/schema`;
14
+ ? `/connections/${encodeURIComponent(connectionId)}/objects`
15
+ : `/connections/${encodeURIComponent(connectionId)}/schema`;
14
16
  const response = await request(session.baseUrl, path, { token: session.token });
15
17
  stdout.write(`${JSON.stringify(response, null, 2)}\n`);
16
18
  }