Skip to content

Commit 989ddd9

Browse files
feat(packages/database-studio): initial view only database viewer (#16)
* add database studio poc * shadcn ui * fix build * studio improvements * tidy deps
1 parent d87e64e commit 989ddd9

42 files changed

Lines changed: 3418 additions & 29 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/breezy-garlics-kiss.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@bunny.net/database-studio": patch
3+
---
4+
5+
- Fix `Input` component not passing props (`value`, `onChange`, `min`, `max`, etc.) to the underlying `<input>` element — missing `{...props}` spread
6+
- Add server-side sorting via `sort` and `order` query params on the `/rows` API endpoint (replaces client-side sorting)
7+
- Add copy-to-clipboard button on table cells (appears on row hover, shows checkmark confirmation)
8+
- Add column visibility toggle with dropdown menu in the toolbar
9+
- Add OR filter logic — filters can now be combined with AND or OR via a `ButtonGroup` toggle
10+
- Add FK badge on foreign key column headers (matches existing PK badge style)
11+
- Add refresh button to re-fetch the current table data
12+
- Replace raw `<select>` elements in filter bar with shadcn `Select` component
13+
- Replace raw `<input>` element in filter bar with shadcn `Input` component
14+
- Replace native checkbox in columns dropdown with shadcn `Checkbox` component
15+
- Add shadcn `ButtonGroup`, `Select`, and `Checkbox` UI components
16+
- Add `--dev` flag (hidden) to `db studio` command to spawn Vite dev server with HMR
17+
- Add loading state and table list to empty studio on launch

.changeset/dull-parents-stare.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bunny.net/cli": patch
3+
---
4+
5+
Hide `registries` command from help output and landing page (moved to experimental commands)

bun.lock

Lines changed: 463 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@bunny.net/api": "workspace:*",
1414
"@bunny.net/app-config": "workspace:*",
1515
"@bunny.net/database-shell": "workspace:*",
16+
"@bunny.net/database-studio": "workspace:*",
1617
"@libsql/client": "^0.17.0",
1718
"@types/prompts": "^2.4.9",
1819
"@types/yargs": "^17.0.35",

packages/cli/src/cli.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,13 @@ const commands: CommandModule[] = [
2222
whoamiCommand,
2323
dbNamespace,
2424
scriptsNamespace,
25-
registriesNamespace,
2625
configNamespace,
2726
docsCommand,
2827
apiCommand,
2928
];
3029

3130
// Experimental commands — registered but hidden from help and landing page
32-
const experimentalCommands: CommandModule[] = [appsNamespace];
31+
const experimentalCommands: CommandModule[] = [appsNamespace, registriesNamespace];
3332

3433
let instance = yargs(hideBin(process.argv))
3534
.scriptName("bunny")

packages/cli/src/commands/db/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import { dbListCommand } from "./list.ts";
66
import { dbQuickstartCommand } from "./quickstart.ts";
77
import { dbShellCommand } from "./shell.ts";
88
import { dbShowCommand } from "./show.ts";
9+
import { dbStudioCommand } from "./studio.ts";
910
import { dbUsageCommand } from "./usage.ts";
1011
import { dbRegionsNamespace } from "./regions/index.ts";
1112
import { dbTokensNamespace } from "./tokens/index.ts";
1213

1314
export const dbNamespace = defineNamespace(
1415
"db",
1516
"Manage databases.",
16-
[dbCreateCommand, dbDeleteCommand, dbDocsCommand, dbListCommand, dbQuickstartCommand, dbRegionsNamespace, dbShellCommand, dbShowCommand, dbUsageCommand, dbTokensNamespace],
17+
[dbCreateCommand, dbDeleteCommand, dbDocsCommand, dbListCommand, dbQuickstartCommand, dbRegionsNamespace, dbShellCommand, dbShowCommand, dbStudioCommand, dbUsageCommand, dbTokensNamespace],
1718
);
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { defineCommand } from "../../core/define-command.ts";
2+
import { resolveConfig } from "../../config/index.ts";
3+
import { createDbClient } from "@bunny.net/api";
4+
import { resolveDbId } from "./resolve-db.ts";
5+
import { spinner } from "../../core/ui.ts";
6+
import { logger } from "../../core/logger.ts";
7+
import { UserError } from "../../core/errors.ts";
8+
import { readEnvValue } from "../../utils/env-file.ts";
9+
import { ARG_DATABASE_ID, ENV_DATABASE_URL, ENV_DATABASE_AUTH_TOKEN } from "./constants.ts";
10+
import { clientOptions } from "../../core/client-options.ts";
11+
12+
const COMMAND = `studio [${ARG_DATABASE_ID}]`;
13+
const DESCRIPTION = "Open a visual database explorer in your browser.";
14+
15+
const ARG_PORT = "port";
16+
const ARG_URL = "url";
17+
const ARG_TOKEN = "token";
18+
const ARG_NO_OPEN = "no-open";
19+
const ARG_DEV = "dev";
20+
21+
/**
22+
* Resolve database credentials — same pattern as shell.ts.
23+
*/
24+
async function resolveCredentials(
25+
urlArg: string | undefined,
26+
tokenArg: string | undefined,
27+
databaseIdArg: string | undefined,
28+
profile: string,
29+
apiKeyOverride?: string,
30+
verbose = false,
31+
): Promise<{ url: string; token: string; databaseId: string | undefined }> {
32+
let url = urlArg ?? readEnvValue(ENV_DATABASE_URL)?.value;
33+
let token = tokenArg ?? readEnvValue(ENV_DATABASE_AUTH_TOKEN)?.value;
34+
35+
if (url && token) return { url, token, databaseId: databaseIdArg };
36+
37+
const config = resolveConfig(profile, apiKeyOverride);
38+
const apiClient = createDbClient(clientOptions(config, verbose));
39+
40+
const { id: databaseId } = await resolveDbId(apiClient, databaseIdArg);
41+
42+
const spin = spinner("Connecting...");
43+
spin.start();
44+
45+
const fetches: Promise<any>[] = [];
46+
47+
if (!url) {
48+
fetches.push(
49+
apiClient.GET("/v2/databases/{db_id}", {
50+
params: { path: { db_id: databaseId } },
51+
}),
52+
);
53+
} else {
54+
fetches.push(Promise.resolve(null));
55+
}
56+
57+
if (!token) {
58+
spin.text = "Generating token...";
59+
fetches.push(
60+
apiClient.PUT("/v2/databases/{db_id}/auth/generate", {
61+
params: { path: { db_id: databaseId } },
62+
body: { authorization: "full-access", expires_at: null },
63+
}),
64+
);
65+
}
66+
67+
const [dbResult, tokenResult] = await Promise.all(fetches);
68+
69+
spin.stop();
70+
71+
if (!url && dbResult) url = dbResult.data?.db?.url;
72+
if (!token && tokenResult) token = tokenResult.data?.token;
73+
74+
if (!url || !token) {
75+
throw new UserError("Could not resolve database URL or generate token.");
76+
}
77+
78+
return { url, token, databaseId };
79+
}
80+
81+
export const dbStudioCommand = defineCommand<{
82+
[ARG_DATABASE_ID]?: string;
83+
[ARG_PORT]?: number;
84+
[ARG_URL]?: string;
85+
[ARG_TOKEN]?: string;
86+
[ARG_NO_OPEN]?: boolean;
87+
[ARG_DEV]?: boolean;
88+
}>({
89+
command: COMMAND,
90+
describe: DESCRIPTION,
91+
examples: [
92+
["$0 db studio", "Open studio (auto-detect from .env)"],
93+
["$0 db studio --port 3000", "Use a custom port"],
94+
["$0 db studio db_abc123", "Open studio for a specific database"],
95+
],
96+
97+
builder: (yargs) =>
98+
yargs
99+
.positional(ARG_DATABASE_ID, {
100+
type: "string",
101+
describe:
102+
"Database ID (db_<ulid>). Auto-detected from BUNNY_DATABASE_URL in .env if omitted.",
103+
})
104+
.option(ARG_PORT, {
105+
type: "number",
106+
default: 4488,
107+
describe: "Port for the studio server",
108+
})
109+
.option(ARG_URL, {
110+
type: "string",
111+
describe: "Database URL (skips API lookup)",
112+
})
113+
.option(ARG_TOKEN, {
114+
type: "string",
115+
describe: "Auth token (skips token generation)",
116+
})
117+
.option(ARG_NO_OPEN, {
118+
type: "boolean",
119+
default: false,
120+
describe: "Don't automatically open the browser",
121+
})
122+
.option(ARG_DEV, {
123+
type: "boolean",
124+
default: false,
125+
hidden: true,
126+
}),
127+
128+
handler: async ({
129+
[ARG_DATABASE_ID]: databaseIdArg,
130+
[ARG_PORT]: port,
131+
[ARG_URL]: urlArg,
132+
[ARG_TOKEN]: tokenArg,
133+
[ARG_NO_OPEN]: noOpen,
134+
[ARG_DEV]: dev,
135+
profile,
136+
verbose,
137+
apiKey,
138+
}) => {
139+
const { createClient } = await import("@libsql/client/web");
140+
const { startStudio } = await import("@bunny.net/database-studio");
141+
142+
const { url, token } = await resolveCredentials(
143+
urlArg,
144+
tokenArg,
145+
databaseIdArg,
146+
profile,
147+
apiKey,
148+
verbose,
149+
);
150+
151+
const client = createClient({ url, authToken: token });
152+
153+
logger.log("");
154+
await startStudio({
155+
client,
156+
port: port ?? 4488,
157+
open: !noOpen,
158+
dev,
159+
logger: {
160+
log: (msg: string) => logger.log(msg),
161+
error: (msg: string) => logger.error(msg),
162+
},
163+
});
164+
},
165+
});

packages/cli/src/commands/registries/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { registryRemoveCommand } from "./remove.ts";
55

66
export const registriesNamespace: CommandModule = {
77
command: "registries",
8-
describe: "Manage container registries.",
8+
describe: false as never,
99
builder: (yargs) => {
1010
yargs.command(registryAddCommand);
1111
yargs.command(registryListCommand);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist/
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema.json",
3+
"style": "new-york",
4+
"rsc": false,
5+
"tsx": true,
6+
"aliases": {
7+
"components": "@/components",
8+
"utils": "@/lib/utils",
9+
"ui": "@/components/ui",
10+
"lib": "@/lib",
11+
"hooks": "@/hooks"
12+
},
13+
"tailwind": {
14+
"config": "",
15+
"css": "src/index.css",
16+
"baseColor": "zinc",
17+
"cssVariables": true,
18+
"prefix": ""
19+
}
20+
}

0 commit comments

Comments
 (0)