Skip to content

Commit 916c5d7

Browse files
feat(database-ui): use code mirror for query editor
1 parent d965b73 commit 916c5d7

File tree

10 files changed

+732
-153
lines changed

10 files changed

+732
-153
lines changed

packages/cli/src/projects/db-studio/api/server.ts

Lines changed: 93 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/// <reference path="../../../../../types/index.d.ts" />
22

33
import type { Database } from "elide:sqlite";
4+
import { Database as DatabaseConstructor } from "elide:sqlite";
45
import type { DiscoveredDatabase } from "./database.ts";
56
import { getDatabaseInfo, getTables, getTableData } from "./database.ts";
67

@@ -12,7 +13,7 @@ type ApiResponse = {
1213

1314
type RouteContext = {
1415
databases: DiscoveredDatabase[];
15-
Database: typeof Database;
16+
Database: typeof DatabaseConstructor;
1617
};
1718

1819
type RouteHandler = (params: Record<string, string>, context: RouteContext, body: string) => Promise<ApiResponse>;
@@ -23,6 +24,15 @@ type Route = {
2324
handler: RouteHandler;
2425
};
2526

27+
type DatabaseHandlerContext = {
28+
database: DiscoveredDatabase;
29+
db: Database;
30+
databases: DiscoveredDatabase[];
31+
Database: typeof DatabaseConstructor;
32+
};
33+
34+
type DatabaseHandler = (params: Record<string, string>, context: DatabaseHandlerContext, body: string) => Promise<ApiResponse>;
35+
2636
function jsonResponse(data: unknown, status: number = 200): ApiResponse {
2737
console.log("returning json response", data);
2838
return {
@@ -36,7 +46,7 @@ function errorResponse(message: string, status: number = 500): ApiResponse {
3646
return jsonResponse({ error: message }, status);
3747
}
3848

39-
function validateAndGetDatabase(
49+
function validateDatabaseIndex(
4050
dbIndexStr: string,
4151
databases: DiscoveredDatabase[]
4252
): { database: DiscoveredDatabase } | { error: ApiResponse } {
@@ -53,6 +63,31 @@ function validateAndGetDatabase(
5363
return { database: databases[dbIndex] };
5464
}
5565

66+
function withDatabase(handler: DatabaseHandler): RouteHandler {
67+
return async (params: Record<string, string>, context: RouteContext, body: string): Promise<ApiResponse> => {
68+
const result = validateDatabaseIndex(params.dbIndex, context.databases);
69+
if ("error" in result) return result.error;
70+
71+
const { database } = result;
72+
const db = new context.Database(database.path);
73+
74+
return handler(params, { ...context, database, db }, body);
75+
};
76+
}
77+
78+
function requireTableName(params: Record<string, string>): ApiResponse | null {
79+
const tableName = params.tableName;
80+
if (!tableName) {
81+
return errorResponse("Table name is required", 400);
82+
}
83+
return null;
84+
}
85+
86+
function handleDatabaseError(err: unknown, operation: string): ApiResponse {
87+
const errorMessage = err instanceof Error ? err.message : "Unknown error";
88+
return errorResponse(`Failed to ${operation}: ${errorMessage}`, 500);
89+
}
90+
5691
function buildWhereClause(where: Record<string, unknown>): { clause: string; values: unknown[] } {
5792
const conditions: string[] = [];
5893
const values: unknown[] = [];
@@ -121,61 +156,36 @@ async function listDatabases(_params: Record<string, string>, context: RouteCont
121156
return jsonResponse({ databases: context.databases });
122157
}
123158

124-
async function getDatabaseInfoRoute(params: Record<string, string>, context: RouteContext, _body: string): Promise<ApiResponse> {
125-
const result = validateAndGetDatabase(params.dbIndex, context.databases);
126-
if ("error" in result) return result.error;
127-
128-
const { database } = result;
129-
const db = new context.Database(database.path);
130-
const info = getDatabaseInfo(db, database.path);
159+
const getDatabaseInfoRoute = withDatabase(async (_params, context, _body) => {
160+
const info = getDatabaseInfo(context.db, context.database.path);
131161

132162
const fullInfo = {
133163
...info,
134-
size: database.size,
135-
lastModified: database.lastModified,
164+
size: context.database.size,
165+
lastModified: context.database.lastModified,
136166
tableCount: info.tableCount,
137167
};
138168

139169
return jsonResponse(fullInfo);
140-
}
141-
142-
async function getTablesRoute(params: Record<string, string>, context: RouteContext, _body: string): Promise<ApiResponse> {
143-
const result = validateAndGetDatabase(params.dbIndex, context.databases);
144-
if ("error" in result) return result.error;
145-
146-
const { database } = result;
147-
const db = new context.Database(database.path);
148-
const tables = getTables(db);
170+
});
149171

172+
const getTablesRoute = withDatabase(async (_params, context, _body) => {
173+
const tables = getTables(context.db);
150174
return jsonResponse({ tables });
151-
}
152-
153-
async function getTableDataRoute(params: Record<string, string>, context: RouteContext, _body: string): Promise<ApiResponse> {
154-
const result = validateAndGetDatabase(params.dbIndex, context.databases);
155-
if ("error" in result) return result.error;
175+
});
156176

157-
const tableName = params.tableName;
158-
if (!tableName) {
159-
return errorResponse("Table name is required", 400);
160-
}
161-
162-
const { database } = result;
163-
const db = new context.Database(database.path);
164-
const tableData = getTableData(db, tableName);
177+
const getTableDataRoute = withDatabase(async (params, context, _body) => {
178+
const tableNameError = requireTableName(params);
179+
if (tableNameError) return tableNameError;
165180

181+
const tableData = getTableData(context.db, params.tableName);
166182
console.log(tableData);
167-
168183
return jsonResponse(tableData);
169-
}
170-
171-
async function insertRowsRoute(params: Record<string, string>, context: RouteContext, body: string): Promise<ApiResponse> {
172-
const result = validateAndGetDatabase(params.dbIndex, context.databases);
173-
if ("error" in result) return result.error;
184+
});
174185

175-
const tableName = params.tableName;
176-
if (!tableName) {
177-
return errorResponse("Table name is required", 400);
178-
}
186+
const insertRowsRoute = withDatabase(async (params, context, body) => {
187+
const tableNameError = requireTableName(params);
188+
if (tableNameError) return tableNameError;
179189

180190
const data = parseRequestBody(body);
181191
const values = data.values as Record<string, unknown> | undefined;
@@ -184,31 +194,22 @@ async function insertRowsRoute(params: Record<string, string>, context: RouteCon
184194
return errorResponse("Request body must contain 'values' object", 400);
185195
}
186196

187-
const { database } = result;
188-
const db = new context.Database(database.path);
189-
190197
const columns = Object.keys(values);
191198
const placeholders = columns.map(() => "?").join(", ");
192-
const sql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
199+
const sql = `INSERT INTO ${params.tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
193200

194201
try {
195-
const stmt = db.prepare(sql);
196-
stmt.run(...Object.values(values));
202+
const stmt = context.db.prepare(sql);
203+
stmt.run(...(Object.values(values) as any));
197204
return jsonResponse({ success: true, message: "Row inserted successfully" });
198205
} catch (err) {
199-
const errorMessage = err instanceof Error ? err.message : "Unknown error";
200-
return errorResponse(`Failed to insert row: ${errorMessage}`, 500);
206+
return handleDatabaseError(err, "insert row");
201207
}
202-
}
208+
});
203209

204-
async function updateRowsRoute(params: Record<string, string>, context: RouteContext, body: string): Promise<ApiResponse> {
205-
const result = validateAndGetDatabase(params.dbIndex, context.databases);
206-
if ("error" in result) return result.error;
207-
208-
const tableName = params.tableName;
209-
if (!tableName) {
210-
return errorResponse("Table name is required", 400);
211-
}
210+
const updateRowsRoute = withDatabase(async (params, context, body) => {
211+
const tableNameError = requireTableName(params);
212+
if (tableNameError) return tableNameError;
212213

213214
const data = parseRequestBody(body);
214215
const values = data.values as Record<string, unknown> | undefined;
@@ -222,32 +223,23 @@ async function updateRowsRoute(params: Record<string, string>, context: RouteCon
222223
return errorResponse("Request body must contain 'where' object with at least one condition", 400);
223224
}
224225

225-
const { database } = result;
226-
const db = new context.Database(database.path);
227-
228226
const setColumns = Object.keys(values).map(key => `${key} = ?`).join(", ");
229227
const { clause: whereClause, values: whereValues } = buildWhereClause(where);
230-
const sql = `UPDATE ${tableName} SET ${setColumns} ${whereClause}`;
228+
const sql = `UPDATE ${params.tableName} SET ${setColumns} ${whereClause}`;
231229

232230
try {
233-
const stmt = db.prepare(sql);
231+
const stmt = context.db.prepare(sql);
234232
const allValues = [...Object.values(values), ...whereValues];
235-
const info = stmt.run(...allValues);
233+
const info = stmt.run(...(allValues as any));
236234
return jsonResponse({ success: true, rowsAffected: info.changes, message: "Rows updated successfully" });
237235
} catch (err) {
238-
const errorMessage = err instanceof Error ? err.message : "Unknown error";
239-
return errorResponse(`Failed to update rows: ${errorMessage}`, 500);
236+
return handleDatabaseError(err, "update rows");
240237
}
241-
}
238+
});
242239

243-
async function deleteRowsRoute(params: Record<string, string>, context: RouteContext, body: string): Promise<ApiResponse> {
244-
const result = validateAndGetDatabase(params.dbIndex, context.databases);
245-
if ("error" in result) return result.error;
246-
247-
const tableName = params.tableName;
248-
if (!tableName) {
249-
return errorResponse("Table name is required", 400);
250-
}
240+
const deleteRowsRoute = withDatabase(async (params, context, body) => {
241+
const tableNameError = requireTableName(params);
242+
if (tableNameError) return tableNameError;
251243

252244
const data = parseRequestBody(body);
253245
const where = data.where as Record<string, unknown> | undefined;
@@ -256,26 +248,19 @@ async function deleteRowsRoute(params: Record<string, string>, context: RouteCon
256248
return errorResponse("Request body must contain 'where' object with at least one condition (safety check)", 400);
257249
}
258250

259-
const { database } = result;
260-
const db = new context.Database(database.path);
261-
262251
const { clause: whereClause, values: whereValues } = buildWhereClause(where);
263-
const sql = `DELETE FROM ${tableName} ${whereClause}`;
252+
const sql = `DELETE FROM ${params.tableName} ${whereClause}`;
264253

265254
try {
266-
const stmt = db.prepare(sql);
267-
const info = stmt.run(...whereValues);
255+
const stmt = context.db.prepare(sql);
256+
const info = stmt.run(...(whereValues as any));
268257
return jsonResponse({ success: true, rowsAffected: info.changes, message: "Rows deleted successfully" });
269258
} catch (err) {
270-
const errorMessage = err instanceof Error ? err.message : "Unknown error";
271-
return errorResponse(`Failed to delete rows: ${errorMessage}`, 500);
259+
return handleDatabaseError(err, "delete rows");
272260
}
273-
}
274-
275-
async function createTableRoute(params: Record<string, string>, context: RouteContext, body: string): Promise<ApiResponse> {
276-
const result = validateAndGetDatabase(params.dbIndex, context.databases);
277-
if ("error" in result) return result.error;
261+
});
278262

263+
const createTableRoute = withDatabase(async (_params, context, body) => {
279264
const data = parseRequestBody(body);
280265
const tableName = data.name as string | undefined;
281266
const schema = data.schema as Array<{ name: string; type: string; constraints?: string }> | undefined;
@@ -288,9 +273,6 @@ async function createTableRoute(params: Record<string, string>, context: RouteCo
288273
return errorResponse("Request body must contain 'schema' array with at least one column", 400);
289274
}
290275

291-
const { database } = result;
292-
const db = new context.Database(database.path);
293-
294276
const columns = schema.map(col => {
295277
const constraints = col.constraints ? ` ${col.constraints}` : "";
296278
return `${col.name} ${col.type}${constraints}`;
@@ -299,22 +281,16 @@ async function createTableRoute(params: Record<string, string>, context: RouteCo
299281
const sql = `CREATE TABLE ${tableName} (${columns})`;
300282

301283
try {
302-
db.exec(sql);
284+
context.db.exec(sql);
303285
return jsonResponse({ success: true, message: `Table '${tableName}' created successfully` });
304286
} catch (err) {
305-
const errorMessage = err instanceof Error ? err.message : "Unknown error";
306-
return errorResponse(`Failed to create table: ${errorMessage}`, 500);
287+
return handleDatabaseError(err, "create table");
307288
}
308-
}
289+
});
309290

310-
async function dropTableRoute(params: Record<string, string>, context: RouteContext, body: string): Promise<ApiResponse> {
311-
const result = validateAndGetDatabase(params.dbIndex, context.databases);
312-
if ("error" in result) return result.error;
313-
314-
const tableName = params.tableName;
315-
if (!tableName) {
316-
return errorResponse("Table name is required", 400);
317-
}
291+
const dropTableRoute = withDatabase(async (params, context, body) => {
292+
const tableNameError = requireTableName(params);
293+
if (tableNameError) return tableNameError;
318294

319295
const data = parseRequestBody(body);
320296
const confirm = data.confirm as boolean | undefined;
@@ -323,24 +299,17 @@ async function dropTableRoute(params: Record<string, string>, context: RouteCont
323299
return errorResponse("Must set 'confirm: true' in request body to drop table (safety check)", 400);
324300
}
325301

326-
const { database } = result;
327-
const db = new context.Database(database.path);
328-
329-
const sql = `DROP TABLE ${tableName}`;
302+
const sql = `DROP TABLE ${params.tableName}`;
330303

331304
try {
332-
db.exec(sql);
333-
return jsonResponse({ success: true, message: `Table '${tableName}' dropped successfully` });
305+
context.db.exec(sql);
306+
return jsonResponse({ success: true, message: `Table '${params.tableName}' dropped successfully` });
334307
} catch (err) {
335-
const errorMessage = err instanceof Error ? err.message : "Unknown error";
336-
return errorResponse(`Failed to drop table: ${errorMessage}`, 500);
308+
return handleDatabaseError(err, "drop table");
337309
}
338-
}
339-
340-
async function executeQueryRoute(params: Record<string, string>, context: RouteContext, body: string): Promise<ApiResponse> {
341-
const result = validateAndGetDatabase(params.dbIndex, context.databases);
342-
if ("error" in result) return result.error;
310+
});
343311

312+
const executeQueryRoute = withDatabase(async (_params, context, body) => {
344313
const data = parseRequestBody(body);
345314
const sql = data.sql as string | undefined;
346315
const queryParams = data.params as unknown[] | undefined;
@@ -353,33 +322,29 @@ async function executeQueryRoute(params: Record<string, string>, context: RouteC
353322
console.warn(`Warning: Executing potentially destructive query: ${sql}`);
354323
}
355324

356-
const { database } = result;
357-
const db = new context.Database(database.path);
358-
359325
try {
360-
const stmt = db.prepare(sql);
326+
const stmt = context.db.prepare(sql);
361327
const params = queryParams || [];
362328

363329
if (sql.trim().toLowerCase().startsWith("select")) {
364-
const rows = stmt.all(...params);
330+
const rows = stmt.all(...(params as any));
365331
return jsonResponse({
366332
success: true,
367333
rows,
368334
rowCount: Array.isArray(rows) ? rows.length : 0,
369335
});
370336
} else {
371-
const info = stmt.run(...params);
337+
const info = stmt.run(...(params as any));
372338
return jsonResponse({
373339
success: true,
374340
rowsAffected: info.changes,
375341
lastInsertRowid: info.lastInsertRowid,
376342
});
377343
}
378344
} catch (err) {
379-
const errorMessage = err instanceof Error ? err.message : "Unknown error";
380-
return errorResponse(`Failed to execute query: ${errorMessage}`, 500);
345+
return handleDatabaseError(err, "execute query");
381346
}
382-
}
347+
});
383348

384349
/**
385350
* Route Registry
@@ -412,7 +377,7 @@ export async function handleApiRequest(
412377
method: string,
413378
body: string,
414379
databases: DiscoveredDatabase[],
415-
Database: typeof Database
380+
Database: typeof DatabaseConstructor
416381
): Promise<ApiResponse> {
417382
// Parse URL path (remove query string if present)
418383
const path = url.split('?')[0];

0 commit comments

Comments
 (0)