Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e34fdec
initial skeleton for db studio command
franklinfollis Oct 17, 2025
d266f39
feat(database-ui): have `elide db studio` be a simple wrapper around …
franklinfollis Oct 21, 2025
9230fc9
feat(database-ui): have db studio init a react ssr template for studi…
franklinfollis Oct 25, 2025
15aba44
feat(database-ui): discover db files on disk and select between them
franklinfollis Oct 26, 2025
b872ce3
feat(database-ui): static react app calling on database json api
franklinfollis Oct 28, 2025
60d6df1
feat(database-ui): consolidate api and ui for db studio into one folder
franklinfollis Oct 29, 2025
f7062c7
feat(database-ui): add shadcn and tailwind to studio frontend
franklinfollis Oct 29, 2025
21ed077
feat(database-ui): standardize json api paths
franklinfollis Oct 29, 2025
6e6132d
feat(database-ui): denote columns that are primary keys in tables
franklinfollis Oct 31, 2025
c77e4fc
fix(database-ui): add primaryKeys to TableData type
franklinfollis Oct 31, 2025
6a4b429
fix(database-ui): move built db studio code under .dev
franklinfollis Nov 4, 2025
5281d6d
fix(database-ui): use new elide http server
franklinfollis Nov 10, 2025
d90925d
feat(database-ui): cleanup server.ts
franklinfollis Nov 10, 2025
7e2e6ef
fix(database-ui): use byte length for content length header
franklinfollis Nov 10, 2025
d965b73
feat(database-ui): provide directory as well as db file path for data…
franklinfollis Nov 10, 2025
916c5d7
feat(database-ui): use code mirror for query editor
franklinfollis Nov 11, 2025
4d396c7
feat(database-ui): move db-studio src out of samples
franklinfollis Nov 11, 2025
c34ddaa
feat(database-ui): setup elide.pkl manifest for api
franklinfollis Nov 12, 2025
2f43eae
feat(database-ui): spread server routes and handlers out to separate …
franklinfollis Nov 12, 2025
8f31eeb
feat(database-ui): add tsconfig to resolve elide:sqlite types
franklinfollis Nov 12, 2025
71c2099
chore(database-ui): cleanup responses and router code
franklinfollis Nov 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
.dev/dependencies
.dev/coverage
.dev/artifacts
.dev/db-studio
*/.dev/artifacts
*/.dev/coverage
*/.dev/dependencies
Expand Down
21 changes: 20 additions & 1 deletion packages/cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2407,7 +2407,7 @@ tasks {
val allSamples = layout.projectDirectory.dir("src/projects")
.asFile
.listFiles()
.filter { it.isDirectory() }
.filter { it.isDirectory() && it.name != "db-studio" }
.map { it.toPath() to it.name }

val builtSamples = layout.buildDirectory.dir("packed-samples")
Expand All @@ -2429,10 +2429,29 @@ tasks {
dependsOn(allSamplePackTasks)
}

val prepareDbStudioResources by registering(Copy::class) {
group = "build"
description = "Prepare Database Studio resources for embedding in CLI"

// Copy API files
from(layout.projectDirectory.dir("src/db-studio/api")) {
into("api")
exclude("config.ts") // Generated at runtime with injected config
}

// Copy built UI (dist/ folder only, not source or node_modules)
from(layout.projectDirectory.dir("src/db-studio/ui/dist")) {
into("ui")
}

into(layout.buildDirectory.dir("resources/main/META-INF/elide/db-studio"))
}

processResources {
dependsOn(
":packages:graalvm:buildRustNativesForHostDebug",
prepKotlinResources,
prepareDbStudioResources,
packSamples,
allSamplePackTasks,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "db-studio-api",
"version": "1.0.0",
"description": "Database Studio API Server",
"main": "index.ts",
"scripts": {},
"dependencies": {
"zod": "4"
},
"devDependencies": {
"@elide-dev/types": "1.0.0-beta10"
}
}
Binary file added packages/cli/src/db-studio/api/.dev/elide.lock.bin
Binary file not shown.
22 changes: 22 additions & 0 deletions packages/cli/src/db-studio/api/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { DiscoveredDatabase } from "./database.ts";

/**
* Database Studio Configuration
*
* This is a sample configuration file. In production, this would be
* generated by DbStudioCommand.kt based on discovered databases.
*/

const config = {
port: 4984,
databases: [
{
path: "./sample.db",
name: "sample.db",
size: 0,
lastModified: Date.now(),
}
] as DiscoveredDatabase[],
};

export default config;
176 changes: 176 additions & 0 deletions packages/cli/src/db-studio/api/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* Database API Layer
*
* Provides abstraction over database operations for the DB Studio.
* This layer isolates all database-specific logic, making it easy to:
* - Swap SQLite for other databases (PostgreSQL, MySQL, etc.)
* - Add caching, connection pooling, or other optimizations
* - Centralize error handling and validation
*
* NOTE: This module does NOT import "elide:sqlite" directly because Elide's
* module loader can only handle that special protocol in the entry point file.
* The Database class must be passed from index.ts.
*/

import type { Database, Statement } from "elide:sqlite";

/**
* Log SQL queries to console
*/
function logQuery(sql: string, params?: unknown[]): void {
const timestamp = new Date().toISOString();
const paramsStr = params && params.length > 0 ? ` [${params.join(", ")}]` : "";
console.log(`[${timestamp}] SQL: ${sql}${paramsStr}`);
}

export interface DiscoveredDatabase {
path: string;
name: string;
size: number;
lastModified: number;
}

export interface TableInfo {
name: string;
rowCount: number;
}

export type TableData = {
name: string;
columns: string[];
rows: unknown[][];
totalRows: number;
};

export interface DatabaseInfo {
path: string;
name: string;
size: number;
lastModified: number;
tableCount: number;
}

interface TableNameRow {
name: string;
}

interface CountRow {
count: number;
}

/**
* Get list of tables in a database
*/
export function getTables(db: Database): TableInfo[] {
const sql = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
logQuery(sql);
const query: Statement<TableNameRow> = db.query(sql);
const results = query.all();

return results.map(({ name }) => {
const tableName = name;
const countSql = `SELECT COUNT(*) as count FROM ${tableName}`;
logQuery(countSql);
const countQuery: Statement<CountRow> = db.query(countSql);
const countResult = countQuery.get();

return {
name: tableName,
rowCount: countResult?.count ?? 0,
};
});
}


interface ColumnNameRow {
name: string;
}

/**
* Get table data with schema and rows
*/
export function getTableData(db: Database, tableName: string, limit: number = 100, offset: number = 0): TableData {

// Get schema (column names)
const schemaSql = `SELECT name FROM pragma_table_info('${tableName}') ORDER BY cid`;
logQuery(schemaSql);
const schemaQuery: Statement<ColumnNameRow> = db.prepare(schemaSql);
const schemaResults = schemaQuery.all();
const columns = schemaResults.map((col) => col.name);

// Get data rows (unknown type since we don't know the schema)
const dataSql = `SELECT * FROM ${tableName} LIMIT ${limit} OFFSET ${offset}`;
logQuery(dataSql);
const dataQuery = db.query(dataSql);
const rows = dataQuery.all();

// Get total row count
const countSql = `SELECT COUNT(*) as count FROM ${tableName}`;
logQuery(countSql);
const countQuery: Statement<CountRow> = db.query(countSql);
const countResult = countQuery.get();
const totalRows = countResult?.count ?? 0;

return {
name: tableName,
columns,
rows: rows.map((row: unknown) => columns.map(col => (row as Record<string, unknown>)[col])),
totalRows,
};
}

/**
* Get database metadata
*/
export function getDatabaseInfo(db: Database, dbPath: string): DatabaseInfo {
const sql = "SELECT COUNT(*) as count FROM sqlite_master WHERE type='table'";
logQuery(sql);
const tablesQuery: Statement<CountRow> = db.query(sql);
const tablesResult = tablesQuery.get();

// Extract name from path
const pathParts = dbPath.split('/');
const name = pathParts[pathParts.length - 1];

return {
path: dbPath,
name,
size: 0, // Will be populated by calling code if available
lastModified: 0, // Will be populated by calling code if available
tableCount: tablesResult?.count ?? 0,
};
}

/**
* Execute a raw SQL query (for future query editor feature)
*/
export function executeQuery(db: Database, sql: string, limit: number = 100): { columns: string[], rows: unknown[][] } {
logQuery(sql);
const query = db.query(sql);
const results = query.all();

if (results.length === 0) {
return { columns: [], rows: [] };
}

const firstRow = results[0] as Record<string, unknown>;
const columns = Object.keys(firstRow);
const rows = results.map((row: unknown) => columns.map(col => (row as Record<string, unknown>)[col]));

return { columns, rows };
}

/**
* Validate that a database path is accessible
*/
export function validateDatabase(db: Database): boolean {
try {
// Try a simple query to verify the database is valid
const sql = "SELECT 1";
const query = db.query(sql);
query.get();
return true;
} catch (err) {
return false;
}
}
22 changes: 22 additions & 0 deletions packages/cli/src/db-studio/api/elide.pkl
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
amends "elide:project.pkl"
import "elide:JavaScript.pkl" as js

name = "db-studio-api"
version = "1.0.0"
description = "Database Studio API Server"

entrypoint {
"index.ts"
}

dependencies {
npm {
packages {
"zod@4"
}

devPackages {
"@elide-dev/[email protected]"
}
}
}
18 changes: 18 additions & 0 deletions packages/cli/src/db-studio/api/http/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ApiResponse, RouteContext, RouteHandler, DatabaseHandler } from "./types.ts";
import { validateDatabaseIndex } from "../utils/validation.ts";

/**
* Middleware that validates database index and provides database instance to handler
*/
export function withDatabase(handler: DatabaseHandler): RouteHandler {
return async (params: Record<string, string>, context: RouteContext, body: string): Promise<ApiResponse> => {
const result = validateDatabaseIndex(params.dbIndex, context.databases);
if ("error" in result) return result.error;

const { database } = result;
const db = new context.Database(database.path);

return handler(params, { ...context, database, db }, body);
};
}

39 changes: 39 additions & 0 deletions packages/cli/src/db-studio/api/http/responses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { ApiResponse } from "./types.ts";

const SUCCESS_STATUS = 200;
const ERROR_STATUS = 500;
const NOT_FOUND_STATUS = 404;

/**
* Create a JSON response
*/
export function jsonResponse(data: unknown, status: number = SUCCESS_STATUS): ApiResponse {
console.log("returning json response", data);
return {
status,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
};
}

/**
* Create an error response
*/
export function errorResponse(message: string, status: number = ERROR_STATUS): ApiResponse {
return jsonResponse({ error: message }, status);
}

/**
* Handle database operation errors
*/
export function handleDatabaseError(err: unknown, operation: string): ApiResponse {
const errorMessage = err instanceof Error ? err.message : "Unknown error";
return errorResponse(`Failed to ${operation}: ${errorMessage}`, ERROR_STATUS);
}

/**
* Creates a 404 Not Found response
*/
export function notFoundResponse(): ApiResponse {
return jsonResponse({ error: "Not Found" }, NOT_FOUND_STATUS);
}
Loading
Loading