@deepsql/mcp 0.3.0 → 0.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepsql/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "DeepSQL CLI and stdio MCP server for self-hosted deployments",
5
5
  "bin": {
6
6
  "deepsql": "./bin/deepsql.js",
package/src/cli.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * small for the self-hosted distribution and makes it trivial to embed in
8
8
  * scripts. Supports:
9
9
  * - boolean flags: --json, --device, --browser, --no-browser
10
- * - value flags: --url <url>, --token <t>, --connection <id>, --limit 50
10
+ * - value flags: --url <url>, --token <t>, --connection <name>, --limit 50
11
11
  * - subcommands: deepsql connections list, deepsql config show
12
12
  * - positional: deepsql ask "what tables exist?"
13
13
  */
@@ -23,6 +23,7 @@ const COMMANDS = {
23
23
  query: () => require("./commands/query"),
24
24
  explain: () => require("./commands/explain"),
25
25
  schema: () => require("./commands/schema"),
26
+ digest: () => require("./commands/digest"),
26
27
  };
27
28
 
28
29
  const HELP = `deepsql — DeepSQL CLI
@@ -46,14 +47,21 @@ Commands:
46
47
  config path Print the auth file path.
47
48
  mcp Run the stdio MCP server using the saved token.
48
49
  connections list [--json] List database connections.
49
- ask "<question>" --connection <id> [--chat <id>] [--json]
50
+ ask "<question>" --connection <name> [--chat <id>] [--json]
50
51
  Ask DeepSQL a question.
51
- query "<sql>" --connection <id> [--limit <n>] [--timeout-seconds <n>] [--file <path>] [--json]
52
+ query "<sql>" --connection <name> [--limit <n>] [--timeout-seconds <n>] [--file <path>] [--json]
52
53
  Run a read-only SQL query.
53
- explain "<sql>" --connection <id> [--file <path>] [--json]
54
+ explain "<sql>" --connection <name> [--file <path>] [--json]
54
55
  Get an EXPLAIN plan.
55
- schema [tables|objects] --connection <id>
56
+ schema [tables|objects] --connection <name>
56
57
  Dump schema or database objects as JSON.
58
+ digest [N] [--connection <name>] [--json]
59
+ Show the latest DeepSQL digest, or pass a
60
+ number to list the last N (e.g. digest 5).
61
+ digest list [--count N] [--connection <name>] [--json]
62
+ Explicit list form (same as digest <N>).
63
+ digest show <id> [--connection <name>] [--json]
64
+ Show one digest by id.
57
65
 
58
66
  Global options:
59
67
  --url <url> Override the DeepSQL base URL.
@@ -122,6 +130,7 @@ function buildOpts(parsed) {
122
130
  user: f.user || null,
123
131
  project: f.project || null,
124
132
  limit: f.limit,
133
+ count: f.count || f.n || null,
125
134
  timeoutSeconds: f.timeoutSeconds,
126
135
  file: f.file || null,
127
136
  };
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Resolve a user-supplied connection identifier (name or UUID) to the
5
+ * canonical connection ID the backend expects.
6
+ *
7
+ * Backend `/connections` returns rows shaped roughly like:
8
+ * { id: "<uuid>", connectionName: "mylocalpg", databaseType: "postgresql", ... }
9
+ *
10
+ * Resolution rules:
11
+ * - If input matches a UUID pattern, treat it as an ID (one fetch saved).
12
+ * We still verify it exists so we can fail fast with a useful message,
13
+ * but only if a list fetch is cheap — for now, trust UUIDs.
14
+ * - Otherwise, fetch the list and match `connectionName` case-insensitively.
15
+ * - On ambiguous matches (rare — names are not unique by schema constraint),
16
+ * prefer an exact-case match; otherwise raise.
17
+ */
18
+
19
+ const { request } = require("../api/client");
20
+
21
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
22
+
23
+ let cachedList = null;
24
+
25
+ async function listConnections(session) {
26
+ if (cachedList) return cachedList;
27
+ cachedList = await request(session.baseUrl, "/connections", { token: session.token });
28
+ if (!Array.isArray(cachedList)) cachedList = [];
29
+ return cachedList;
30
+ }
31
+
32
+ async function resolveConnectionId(session, input) {
33
+ if (!input || typeof input !== "string") {
34
+ throw new Error("Pass a connection name or id with --connection.");
35
+ }
36
+ const trimmed = input.trim();
37
+ if (UUID_RE.test(trimmed)) return trimmed;
38
+
39
+ const connections = await listConnections(session);
40
+ const exact = connections.filter((c) => (c.connectionName || c.name) === trimmed);
41
+ if (exact.length === 1) return exact[0].id || exact[0].connectionId;
42
+
43
+ const ciMatches = connections.filter(
44
+ (c) => String(c.connectionName || c.name || "").toLowerCase() === trimmed.toLowerCase(),
45
+ );
46
+ if (ciMatches.length === 1) return ciMatches[0].id || ciMatches[0].connectionId;
47
+
48
+ if (ciMatches.length > 1) {
49
+ const names = ciMatches.map((c) => `${c.connectionName} (${c.id})`).join(", ");
50
+ throw new Error(
51
+ `Multiple connections match "${trimmed}" by case-insensitive name: ${names}. Pass the exact name or the id.`,
52
+ );
53
+ }
54
+
55
+ // No match — show what's available so the user can pick.
56
+ const available = connections
57
+ .map((c) => c.connectionName || c.name)
58
+ .filter(Boolean)
59
+ .slice(0, 20);
60
+ const hint = available.length
61
+ ? ` Available: ${available.join(", ")}.`
62
+ : " (no connections visible to this token).";
63
+ throw new Error(`Connection "${trimmed}" not found.${hint}`);
64
+ }
65
+
66
+ module.exports = { resolveConnectionId, listConnections };
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+
3
+ const test = require("node:test");
4
+ const assert = require("node:assert/strict");
5
+
6
+ // We mock api/client.request before requiring the resolver so its cached
7
+ // module reference points at our stub.
8
+ const apiClientPath = require.resolve("../api/client");
9
+ const realApiClient = require("../api/client");
10
+
11
+ function withMockedRequest(connections, fn) {
12
+ delete require.cache[require.resolve("./_connections")];
13
+ require.cache[apiClientPath] = {
14
+ ...require.cache[apiClientPath],
15
+ exports: {
16
+ ...realApiClient,
17
+ request: async () => connections,
18
+ },
19
+ };
20
+ try {
21
+ const mod = require("./_connections");
22
+ return fn(mod);
23
+ } finally {
24
+ require.cache[apiClientPath].exports = realApiClient;
25
+ delete require.cache[require.resolve("./_connections")];
26
+ }
27
+ }
28
+
29
+ const session = { baseUrl: "http://x", token: "t" };
30
+
31
+ test("resolves UUID input as-is without a backend roundtrip", async () => {
32
+ await withMockedRequest([], async ({ resolveConnectionId }) => {
33
+ const id = await resolveConnectionId(session, "a273f43a-a844-44a3-9026-1b0de1167e8f");
34
+ assert.equal(id, "a273f43a-a844-44a3-9026-1b0de1167e8f");
35
+ });
36
+ });
37
+
38
+ test("matches by exact connection name", async () => {
39
+ const connections = [
40
+ { id: "id-prod", connectionName: "prod" },
41
+ { id: "id-staging", connectionName: "staging" },
42
+ ];
43
+ await withMockedRequest(connections, async ({ resolveConnectionId }) => {
44
+ assert.equal(await resolveConnectionId(session, "prod"), "id-prod");
45
+ assert.equal(await resolveConnectionId(session, "staging"), "id-staging");
46
+ });
47
+ });
48
+
49
+ test("matches case-insensitively when no exact match exists", async () => {
50
+ const connections = [{ id: "id-prod", connectionName: "MyLocalPG" }];
51
+ await withMockedRequest(connections, async ({ resolveConnectionId }) => {
52
+ assert.equal(await resolveConnectionId(session, "mylocalpg"), "id-prod");
53
+ });
54
+ });
55
+
56
+ test("throws a useful error listing available names when no match", async () => {
57
+ const connections = [
58
+ { id: "1", connectionName: "alpha" },
59
+ { id: "2", connectionName: "beta" },
60
+ ];
61
+ await withMockedRequest(connections, async ({ resolveConnectionId }) => {
62
+ await assert.rejects(
63
+ () => resolveConnectionId(session, "missing"),
64
+ (err) => err.message.includes("alpha") && err.message.includes("beta"),
65
+ );
66
+ });
67
+ });
68
+
69
+ test("requires non-empty input", async () => {
70
+ await withMockedRequest([], async ({ resolveConnectionId }) => {
71
+ await assert.rejects(() => resolveConnectionId(session, ""), /connection name/);
72
+ await assert.rejects(() => resolveConnectionId(session, null), /connection name/);
73
+ });
74
+ });
@@ -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) {
@@ -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
  }